From ac9744a82f73d498da7b8719e5c8e43fbcfe3542 Mon Sep 17 00:00:00 2001 From: mariiaKraievska Date: Tue, 16 Jun 2026 14:43:48 +0300 Subject: [PATCH 1/2] FINERACT-2455: WC - Breach Management - Recalculation when Breach evaluation is changed(breach reschedule) --- .../client/feign/FineractFeignClient.java | 5 + .../service/CommandWrapperBuilder.java | 9 + .../WorkingCapitalLoanRequestFactory.java | 8 + .../WorkingCapitalBreachActionStepDef.java | 201 ++++++++++++ .../WorkingCapitalLoanAccountStepDef.java | 6 +- .../WorkingCapitalBreachReschedule.feature | 296 ++++++++++++++++++ ...ingCapitalLoanBreachActionApiResource.java | 135 ++++++++ ...talLoanBreachActionApiResourceSwagger.java | 64 ++++ .../WorkingCapitalLoanBreachActionData.java | 31 ++ .../WorkingCapitalLoanBreachAction.java | 73 +++++ ...CapitalLoanBreachActionCommandHandler.java | 43 +++ ...kingCapitalLoanBreachActionRepository.java | 34 ++ ...ngCapitalLoanBreachScheduleRepository.java | 2 + ...ingCapitalLoanBreachActionReadService.java | 28 ++ ...apitalLoanBreachActionReadServiceImpl.java | 52 +++ ...ngCapitalLoanBreachActionWriteService.java | 28 ++ ...pitalLoanBreachActionWriteServiceImpl.java | 71 +++++ ...rkingCapitalLoanBreachScheduleService.java | 3 + ...gCapitalLoanBreachScheduleServiceImpl.java | 152 ++++++++- ...italLoanBreachActionParseAndValidator.java | 205 ++++++++++++ .../module-changelog-master.xml | 2 + .../parts/0045_wc_loan_breach_action.xml | 151 +++++++++ .../0046_wc_loan_breach_action_reschedule.xml | 41 +++ .../persistence.xml | 1 + 24 files changed, 1629 insertions(+), 12 deletions(-) create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalBreachActionStepDef.java create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachReschedule.feature create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanBreachActionApiResource.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanBreachActionApiResourceSwagger.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanBreachActionData.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanBreachAction.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/CreateWorkingCapitalLoanBreachActionCommandHandler.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachActionRepository.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionReadService.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionReadServiceImpl.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionWriteService.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionWriteServiceImpl.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/validator/WorkingCapitalLoanBreachActionParseAndValidator.java create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0045_wc_loan_breach_action.xml create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0046_wc_loan_breach_action_reschedule.xml diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java index e2851b5db61..1ed76801d0a 100644 --- a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java +++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java @@ -156,6 +156,7 @@ import org.apache.fineract.client.feign.services.UsersApi; import org.apache.fineract.client.feign.services.WorkingCapitalBreachApi; import org.apache.fineract.client.feign.services.WorkingCapitalLoanAccountLockApi; +import org.apache.fineract.client.feign.services.WorkingCapitalLoanBreachActionsApi; import org.apache.fineract.client.feign.services.WorkingCapitalLoanBreachScheduleApi; import org.apache.fineract.client.feign.services.WorkingCapitalLoanChargesApi; import org.apache.fineract.client.feign.services.WorkingCapitalLoanCobCatchUpApi; @@ -779,6 +780,10 @@ public WorkingCapitalLoanBreachScheduleApi workingCapitalLoanBreachSchedule() { return create(WorkingCapitalLoanBreachScheduleApi.class); } + public WorkingCapitalLoanBreachActionsApi workingCapitalLoanBreachActions() { + return create(WorkingCapitalLoanBreachActionsApi.class); + } + public InternalWorkingCapitalLoansApi internalWorkingCapitalLoans() { return create(InternalWorkingCapitalLoansApi.class); } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index d96301cbf7d..7f4036ad7f2 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -881,6 +881,15 @@ public CommandWrapperBuilder createWorkingCapitalLoanDelinquencyAction(final Lon return this; } + public CommandWrapperBuilder createWorkingCapitalLoanBreachAction(final Long workingCapitalLoanId) { + this.actionName = "CREATE"; + this.entityName = "WC_BREACH_ACTION"; + this.entityId = workingCapitalLoanId; + this.loanId = workingCapitalLoanId; + this.href = "/working-capital-loans/" + workingCapitalLoanId + "/breach-actions"; + return this; + } + public CommandWrapperBuilder updateDiscountWorkingCapitalLoanApplication(final Long loanId) { this.actionName = "UPDATEDISCOUNT"; this.entityName = "WORKINGCAPITALLOAN"; diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalLoanRequestFactory.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalLoanRequestFactory.java index cb87f2b3392..83f99e035c7 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalLoanRequestFactory.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalLoanRequestFactory.java @@ -21,6 +21,7 @@ import java.math.BigDecimal; import java.time.format.DateTimeFormatter; import lombok.RequiredArgsConstructor; +import org.apache.fineract.client.models.PostWorkingCapitalLoansBreachActionRequest; import org.apache.fineract.client.models.PostWorkingCapitalLoansDelinquencyActionRequest; import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdRequest; import org.apache.fineract.client.models.PostWorkingCapitalLoansRequest; @@ -117,6 +118,13 @@ public PostWorkingCapitalLoansDelinquencyActionRequest defaultWorkingCapitalLoan .locale(DEFAULT_LOCALE);// } + public PostWorkingCapitalLoansBreachActionRequest defaultWorkingCapitalLoansBreachActionRequest(String action) { + return new PostWorkingCapitalLoansBreachActionRequest()// + .action(action)// + .dateFormat(DATE_FORMAT)// + .locale(DEFAULT_LOCALE);// + } + public PutWorkingCapitalLoansLoanIdDiscountRequest defaultWorkingCapitalLoanUpdateDiscountRequest() { return new PutWorkingCapitalLoansLoanIdDiscountRequest()// .discountAmount(DEFAULT_DISCOUNT).note("")// diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalBreachActionStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalBreachActionStepDef.java new file mode 100644 index 00000000000..48535070504 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalBreachActionStepDef.java @@ -0,0 +1,201 @@ +/** + * 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.test.stepdef.loan; + +import static org.apache.fineract.client.feign.util.FeignCalls.fail; +import static org.apache.fineract.client.feign.util.FeignCalls.ok; +import static org.assertj.core.api.Assertions.assertThat; + +import io.cucumber.datatable.DataTable; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.feign.FineractFeignClient; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.models.PostWorkingCapitalLoansBreachActionRequest; +import org.apache.fineract.client.models.PostWorkingCapitalLoansBreachActionResponse; +import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse; +import org.apache.fineract.client.models.WorkingCapitalLoanBreachActionData; +import org.apache.fineract.client.models.WorkingCapitalLoanBreachScheduleData; +import org.apache.fineract.test.factory.WorkingCapitalLoanRequestFactory; +import org.apache.fineract.test.stepdef.AbstractStepDef; +import org.apache.fineract.test.support.TestContextKey; + +@Slf4j +@RequiredArgsConstructor +public class WorkingCapitalBreachActionStepDef extends AbstractStepDef { + + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd MMMM yyyy"); + + private final FineractFeignClient fineractFeignClient; + private final WorkingCapitalLoanRequestFactory workingCapitalLoanRequestFactory; + + @When("Admin creates WC breach reschedule action with the following parameters:") + public void createRescheduleAction(final DataTable table) { + final Map params = table.asMaps().getFirst(); + final PostWorkingCapitalLoansBreachActionRequest request = buildRescheduleRequest(params); + executeRescheduleAction(request); + } + + @Then("Admin fails to create WC breach reschedule action with minimumPayment {int} {word} and frequency {int} {word} with error containing {string}") + public void failToCreateRescheduleActionWithMessage(final int minimumPayment, final String minimumPaymentType, final int frequency, + final String frequencyType, final String expectedMessage) { + final Long loanId = getLoanId(); + final PostWorkingCapitalLoansBreachActionRequest request = buildRescheduleRequest(new BigDecimal(minimumPayment), + minimumPaymentType, frequency, frequencyType); + final CallFailedRuntimeException exception = fail( + () -> fineractFeignClient.workingCapitalLoanBreachActions().createBreachAction(loanId, request)); + assertThat(exception.getStatus()).as("HTTP status code").isEqualTo(400); + assertThat(exception.getDeveloperMessage()).as("Developer message").contains(expectedMessage); + } + + @Then("Admin fails to create WC breach reschedule action with no parameters with error containing {string}") + public void failToCreateEmptyRescheduleAction(final String expectedMessage) { + final Long loanId = getLoanId(); + final PostWorkingCapitalLoansBreachActionRequest request = buildRescheduleRequest(Map.of()); + + final CallFailedRuntimeException exception = fail( + () -> fineractFeignClient.workingCapitalLoanBreachActions().createBreachAction(loanId, request)); + assertThat(exception.getStatus()).as("HTTP status code").isEqualTo(400); + assertThat(exception.getDeveloperMessage()).as("Developer message").contains(expectedMessage); + } + + @Then("WC loan breach actions have the following data:") + public void verifyBreachActionsHistory(final DataTable table) { + final Long loanId = getLoanId(); + final List actions = retrieveBreachActions(loanId); + final List> expectedRows = table.asMaps(); + assertThat(actions).as("Breach actions count").hasSize(expectedRows.size()); + for (int i = 0; i < expectedRows.size(); i++) { + final WorkingCapitalLoanBreachActionData actual = actions.get(i); + final int rowNumber = i + 1; + expectedRows.get(i).forEach((field, value) -> verifyActionField(actual, field, value, rowNumber)); + } + log.info("Successfully verified {} breach action(s) for loan {}", actions.size(), loanId); + } + + @Then("Working Capital loan breach schedule periods have specific data:") + public void verifySpecificPeriods(final DataTable table) { + final Long loanId = getLoanId(); + final List periods = ok( + () -> fineractFeignClient.workingCapitalLoanBreachSchedule().retrieveBreachSchedule(loanId)); + + for (final Map expected : table.asMaps()) { + final int periodNumber = Integer.parseInt(expected.get("periodNumber")); + final WorkingCapitalLoanBreachScheduleData actual = periods.stream().filter(p -> { + assert p.getPeriodNumber() != null; + return p.getPeriodNumber().equals(periodNumber); + }).findFirst().orElse(null); + assertThat(actual).as("Period %d should exist", periodNumber).isNotNull(); + expected.forEach((field, value) -> verifyScheduleField(actual, field, value, periodNumber)); + } + } + + private void executeRescheduleAction(final PostWorkingCapitalLoansBreachActionRequest request) { + final Long loanId = getLoanId(); + log.info("Creating breach RESCHEDULE action for WC loan {}: {}", loanId, request); + + final PostWorkingCapitalLoansBreachActionResponse result = ok( + () -> fineractFeignClient.workingCapitalLoanBreachActions().createBreachAction(loanId, request)); + assertThat(result).isNotNull(); + assertThat(result.getResourceId()).isNotNull(); + log.info("Breach RESCHEDULE action created with id={}", result.getResourceId()); + } + + private List retrieveBreachActions(final Long loanId) { + return ok(() -> fineractFeignClient.workingCapitalLoanBreachActions().retrieveBreachActions(loanId)); + } + + private void verifyActionField(final WorkingCapitalLoanBreachActionData actual, final String field, final String expected, + final int rowNumber) { + final String label = "Action " + rowNumber + " " + field; + switch (field) { + case "action" -> { + assert actual.getAction() != null; + assertThat(actual.getAction().name()).as(label).isEqualTo(expected); + } + case "startDate" -> assertThat(actual.getStartDate()).as(label).isEqualTo(LocalDate.parse(expected, DATE_FORMAT)); + case "minimumPayment" -> assertThat(actual.getMinimumPayment()).as(label).isEqualByComparingTo(new BigDecimal(expected)); + case "minimumPaymentType" -> + verifyOptionalField(expected, v -> assertThat(String.valueOf(actual.getMinimumPaymentType())).as(label).isEqualTo(v), + () -> assertThat(actual.getMinimumPaymentType()).as(label).isNull()); + case "frequency" -> assertThat(actual.getFrequency()).as(label).isEqualTo(Integer.parseInt(expected)); + case "frequencyType" -> + verifyOptionalField(expected, v -> assertThat(String.valueOf(actual.getFrequencyType())).as(label).isEqualTo(v), + () -> assertThat(actual.getFrequencyType()).as(label).isNull()); + default -> throw new IllegalArgumentException("Unknown action field: " + field); + } + } + + private void verifyScheduleField(final WorkingCapitalLoanBreachScheduleData actual, final String field, final String expected, + final int periodNumber) { + final String label = "Period " + periodNumber + " " + field; + switch (field) { + case "periodNumber" -> assertThat(actual.getPeriodNumber()).as(label).isEqualTo(Integer.parseInt(expected)); + case "fromDate" -> assertThat(actual.getFromDate()).as(label).isEqualTo(LocalDate.parse(expected)); + case "toDate" -> assertThat(actual.getToDate()).as(label).isEqualTo(LocalDate.parse(expected)); + case "numberOfDays" -> + verifyOptionalField(expected, v -> assertThat(actual.getNumberOfDays()).as(label).isEqualTo(Integer.parseInt(v)), + () -> assertThat(actual.getNumberOfDays()).as(label).isNull()); + case "minPaymentAmount" -> assertThat(actual.getMinPaymentAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)); + case "outstandingAmount" -> assertThat(actual.getOutstandingAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)); + case "nearBreach" -> + verifyOptionalField(expected, v -> assertThat(actual.getNearBreach()).as(label).isEqualTo(Boolean.parseBoolean(v)), + () -> assertThat(actual.getNearBreach()).as(label).isNull()); + case "breach" -> verifyOptionalField(expected, v -> assertThat(actual.getBreach()).as(label).isEqualTo(Boolean.parseBoolean(v)), + () -> assertThat(actual.getBreach()).as(label).isNull()); + default -> throw new IllegalArgumentException("Unknown schedule field: " + field); + } + } + + private void verifyOptionalField(final String expected, final Consumer whenPresent, final Runnable whenAbsent) { + Optional.ofNullable(expected).filter(Predicate.not(String::isBlank)).ifPresentOrElse(whenPresent, whenAbsent); + } + + private Long getLoanId() { + final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + assertThat(loanResponse).isNotNull(); + return loanResponse.getLoanId(); + } + + private PostWorkingCapitalLoansBreachActionRequest buildRescheduleRequest(final BigDecimal minimumPayment, + final String minimumPaymentType, final int frequency, final String frequencyType) { + return buildRescheduleRequest(Map.of("minimumPayment", minimumPayment.toPlainString(), "minimumPaymentType", minimumPaymentType, + "frequency", String.valueOf(frequency), "frequencyType", frequencyType)); + } + + private PostWorkingCapitalLoansBreachActionRequest buildRescheduleRequest(final Map params) { + final PostWorkingCapitalLoansBreachActionRequest request = workingCapitalLoanRequestFactory + .defaultWorkingCapitalLoansBreachActionRequest("reschedule"); + Optional.ofNullable(params.get("minimumPayment")).ifPresent(v -> request.setMinimumPayment(new BigDecimal(v))); + Optional.ofNullable(params.get("minimumPaymentType")).ifPresent(request::setMinimumPaymentType); + Optional.ofNullable(params.get("frequency")).ifPresent(v -> request.setFrequency(Integer.parseInt(v))); + Optional.ofNullable(params.get("frequencyType")).ifPresent(request::setFrequencyType); + return request; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java index 8a29ed15b8b..f53c1de7805 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java @@ -1955,10 +1955,12 @@ private Long extractClientId() { } private Long resolveLoanProductId(final String loanProductName) { - if ("WCLP_DELINQUENCY".equals(loanProductName)) { + if ("WCLP_DELINQUENCY".equals(loanProductName) || "WCLP_BREACH".equals(loanProductName)) { final PostWorkingCapitalLoanProductsResponse response = testContext() .get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE); - return response.getResourceId(); + if (response != null) { + return response.getResourceId(); + } } final DefaultWorkingCapitalLoanProduct product = DefaultWorkingCapitalLoanProduct.valueOf(loanProductName); return workingCapitalLoanProductResolver.resolve(product); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachReschedule.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachReschedule.feature new file mode 100644 index 00000000000..cbe7ba2ce59 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachReschedule.feature @@ -0,0 +1,296 @@ +@WorkingCapital +@WorkingCapitalBreachRescheduleActionFeature @WCCOBFeature +Feature: Working Capital Breach Reschedule Action + + Scenario: Verify that breach reschedule changes minimumPayment only + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a new Working Capital Loan Product with breachId and overrides enabled + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 June 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-02-28 | 59 | 110.70 | 110.70 | null | true | + | 2 | 2026-03-01 | 2026-04-30 | 61 | 110.70 | 110.70 | null | true | + | 3 | 2026-05-01 | 2026-06-30 | 61 | 110.70 | 110.70 | null | null | + When Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | + | 1 | PERCENTAGE | + When Admin sets the business date to "15 August 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2026-01-01 | 2026-02-28 | 110.70 | 110.70 | true | + | 2 | 2026-03-01 | 2026-04-30 | 110.70 | 110.70 | true | + | 3 | 2026-05-01 | 2026-06-30 | 90 | 90 | true | + | 4 | 2026-07-01 | 2026-08-31 | 90 | 90 | | + + Scenario: Verify that breach reschedule changes frequency only + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a new Working Capital Loan Product with breachId and overrides enabled + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 June 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin creates WC breach reschedule action with the following parameters: + | frequency | frequencyType | + | 30 | DAYS | + When Admin sets the business date to "15 August 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2026-01-01 | 2026-02-28 | 110.70 | 110.70 | true | + | 2 | 2026-03-01 | 2026-04-30 | 110.70 | 110.70 | true | + | 3 | 2026-05-01 | 2026-06-30 | 110.70 | 110.70 | true | + | 4 | 2026-07-01 | 2026-07-30 | 110.70 | 110.70 | true | + | 5 | 2026-07-31 | 2026-08-29 | 110.70 | 110.70 | | + + Scenario: Verify that breach reschedule changes minimumPayment and frequency + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a new Working Capital Loan Product with breachId and overrides enabled + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 June 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | frequency | frequencyType | + | 1 | PERCENTAGE | 30 | DAYS | + When Admin sets the business date to "15 August 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2026-01-01 | 2026-02-28 | 110.70 | 110.70 | true | + | 2 | 2026-03-01 | 2026-04-30 | 110.70 | 110.70 | true | + | 3 | 2026-05-01 | 2026-06-30 | 90 | 90 | true | + | 4 | 2026-07-01 | 2026-07-30 | 90 | 90 | true | + | 5 | 2026-07-31 | 2026-08-29 | 90 | 90 | | + + Scenario: Verify that the latest breach reschedule action wins + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a new Working Capital Loan Product with breachId and overrides enabled + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 June 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | frequency | frequencyType | + | 2 | PERCENTAGE | 30 | DAYS | + When Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | frequency | frequencyType | + | 1 | PERCENTAGE | 30 | DAYS | + When Admin sets the business date to "15 August 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 3 | 2026-05-01 | 2026-06-30 | 90 | 90 | true | + | 4 | 2026-07-01 | 2026-07-30 | 90 | 90 | true | + + Scenario: Verify multiple breach reschedules on the same date are stored in history + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a new Working Capital Loan Product with breachId and overrides enabled + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 June 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | frequency | frequencyType | + | 2 | PERCENTAGE | 2 | MONTHS | + When Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | frequency | frequencyType | + | 1 | PERCENTAGE | 2 | MONTHS | + When Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | frequency | frequencyType | + | 1.5 | PERCENTAGE | 2 | MONTHS | + And WC loan breach actions have the following data: + | action | startDate | minimumPayment | minimumPaymentType | frequency | frequencyType | + | RESCHEDULE | 01 June 2026 | 2 | PERCENTAGE | 2 | MONTHS | + | RESCHEDULE | 01 June 2026 | 1 | PERCENTAGE | 2 | MONTHS | + | RESCHEDULE | 01 June 2026 | 1.5 | PERCENTAGE | 2 | MONTHS | + + Scenario: Verify breach reschedule fails when no change parameters are provided + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a new Working Capital Loan Product with breachId and overrides enabled + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + Then Admin fails to create WC breach reschedule action with no parameters with error containing "reschedule.no.change.parameters" + + Scenario: Verify breach reschedule fails with negative minimumPayment + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a new Working Capital Loan Product with breachId and overrides enabled + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + Then Admin fails to create WC breach reschedule action with minimumPayment -1 PERCENTAGE and frequency 30 DAYS with error containing "minimumPayment" + + Scenario: Verify that payment-only reschedule after frequency reschedule falls back to product breach frequency + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a new Working Capital Loan Product with breachId and overrides enabled + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "01 June 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin creates WC breach reschedule action with the following parameters: + | frequency | frequencyType | + | 30 | DAYS | + When Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | + | 1 | PERCENTAGE | + When Admin sets the business date to "15 August 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2026-01-01 | 2026-02-28 | 110.70 | 110.70 | true | + | 2 | 2026-03-01 | 2026-04-30 | 110.70 | 110.70 | true | + | 3 | 2026-05-01 | 2026-06-30 | 90 | 90 | true | + | 4 | 2026-07-01 | 2026-08-31 | 90 | 90 | | + + Scenario: Verify breach reschedule updates current period after partial repayment and replays payments + When Admin sets the business date to "01 January 2019" + And Admin creates a client with random data + And Admin creates a Working Capital Loan Product with custom breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | delinquencyGraceDays | + | 90 | DAYS | PERCENTAGE | 9 | 3 | + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2019 | 01 January 2019 | 9000 | 100000 | 18 | 1000 | + And Admin successfully approves the working capital loan on "01 January 2019" with "9000" amount and expected disbursement date on "01 January 2019" + And Admin successfully disburse the Working Capital loan on "01 January 2019" with "9000" EUR transaction amount and "1000" discount amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "05 March 2019" + And Customer makes repayment on "05 March 2019" with 450.0 transaction amount on Working Capital loan + When Admin sets the business date to "10 March 2019" + And Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | + | 5 | PERCENTAGE | + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2019-01-01 | 2019-03-31 | 500.00 | 50.00 | | + + Scenario: Verify breach reschedule preserves already evaluated periods + When Admin sets the business date to "01 January 2019" + And Admin creates a client with random data + And Admin creates a Working Capital Loan Product with custom breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | delinquencyGraceDays | + | 90 | DAYS | PERCENTAGE | 10 | 3 | + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2019 | 01 January 2019 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2019" with "9000" amount and expected disbursement date on "01 January 2019" + And Admin successfully disburse the Working Capital loan on "01 January 2019" with "9000" EUR transaction amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "05 March 2019" + And Customer makes repayment on "05 March 2019" with 450.0 transaction amount on Working Capital loan + When Admin sets the business date to "06 April 2019" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2019-01-01 | 2019-03-31 | 900.00 | 450.00 | true | + | 2 | 2019-04-01 | 2019-06-29 | 900.00 | 900.00 | | + When Admin sets the business date to "10 April 2019" + And Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | + | 5 | PERCENTAGE | + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2019-01-01 | 2019-03-31 | 900.00 | 450.00 | true | + | 2 | 2019-04-01 | 2019-06-29 | 450.00 | 450.00 | | + + Scenario: Verify breach reschedule changes frequency from 90 days to 30 days for current and future periods + When Admin sets the business date to "01 January 2019" + And Admin creates a client with random data + And Admin creates a Working Capital Loan Product with custom breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | delinquencyGraceDays | + | 90 | DAYS | PERCENTAGE | 9 | 3 | + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2019 | 01 January 2019 | 9000 | 100000 | 18 | 1000 | + And Admin successfully approves the working capital loan on "01 January 2019" with "9000" amount and expected disbursement date on "01 January 2019" + And Admin successfully disburse the Working Capital loan on "01 January 2019" with "9000" EUR transaction amount and "1000" discount amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "10 March 2019" + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2019-01-01 | 2019-03-31 | 900.00 | 900.00 | | + And Admin creates WC breach reschedule action with the following parameters: + | frequency | frequencyType | + | 30 | DAYS | + When Admin sets the business date to "04 April 2019" + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "15 June 2019" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2019-01-01 | 2019-03-31 | 900.00 | 900.00 | true | + | 2 | 2019-04-01 | 2019-04-30 | 900.00 | 900.00 | true | + | 3 | 2019-05-01 | 2019-05-30 | 900.00 | 900.00 | true | + | 4 | 2019-05-31 | 2019-06-29 | 900.00 | 900.00 | | + + Scenario: Verify breach reschedule changes minimum payment and frequency together + When Admin sets the business date to "01 January 2019" + And Admin creates a client with random data + And Admin creates a Working Capital Loan Product with custom breach config and overrides enabled: + | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | delinquencyGraceDays | + | 90 | DAYS | PERCENTAGE | 9 | 3 | + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_BREACH | 01 January 2019 | 01 January 2019 | 9000 | 100000 | 18 | 1000 | + And Admin successfully approves the working capital loan on "01 January 2019" with "9000" amount and expected disbursement date on "01 January 2019" + And Admin successfully disburse the Working Capital loan on "01 January 2019" with "9000" EUR transaction amount and "1000" discount amount + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "10 March 2019" + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2019-01-01 | 2019-03-31 | 900.00 | 900.00 | | + And Admin creates WC breach reschedule action with the following parameters: + | minimumPayment | minimumPaymentType | frequency | frequencyType | + | 5 | PERCENTAGE | 30 | DAYS | + When Admin sets the business date to "04 April 2019" + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "15 June 2019" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan breach schedule periods have specific data: + | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | + | 1 | 2019-01-01 | 2019-03-31 | 500.00 | 500.00 | true | + | 2 | 2019-04-01 | 2019-04-30 | 500.00 | 500.00 | true | + | 3 | 2019-05-01 | 2019-05-30 | 500.00 | 500.00 | true | + | 4 | 2019-05-31 | 2019-06-29 | 500.00 | 500.00 | | diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanBreachActionApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanBreachActionApiResource.java new file mode 100644 index 00000000000..3fbc49daa2c --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanBreachActionApiResource.java @@ -0,0 +1,135 @@ +/** + * 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.workingcapitalloan.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanBreachActionData; +import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanApplicationReadPlatformService; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanBreachActionReadService; +import org.springframework.stereotype.Component; + +@Path("/v1/working-capital-loans") +@Component +@Tag(name = "Working Capital Loan Breach Actions", description = "Manages breach reschedule actions for Working Capital loans") +@RequiredArgsConstructor +public class WorkingCapitalLoanBreachActionApiResource { + + private static final String RESOURCE_NAME_FOR_PERMISSIONS = "WC_BREACH_ACTION"; + + private final PlatformSecurityContext context; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final WorkingCapitalLoanBreachActionReadService readService; + private final WorkingCapitalLoanApplicationReadPlatformService loanReadPlatformService; + + @POST + @Path("{loanId}/breach-actions") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Create Breach Action", description = "Creates a breach reschedule action for a Working Capital loan.") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanBreachActionApiResourceSwagger.PostWorkingCapitalLoansBreachActionRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanBreachActionApiResourceSwagger.PostWorkingCapitalLoansBreachActionResponse.class))), + @ApiResponse(responseCode = "400", description = "Bad Request"), + @ApiResponse(responseCode = "404", description = "Working Capital Loan not found") }) + public CommandProcessingResult createBreachAction(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME_FOR_PERMISSIONS); + final CommandWrapper commandRequest = new CommandWrapperBuilder() // + .createWorkingCapitalLoanBreachAction(loanId) // + .withJson(apiRequestBodyAsJson) // + .build(); + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } + + @POST + @Path("external-id/{loanExternalId}/breach-actions") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "createBreachActionByExternalId", summary = "Create Breach Action by external id", description = "Creates a breach reschedule action for a Working Capital loan identified by external id.") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanBreachActionApiResourceSwagger.PostWorkingCapitalLoansBreachActionRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanBreachActionApiResourceSwagger.PostWorkingCapitalLoansBreachActionResponse.class))), + @ApiResponse(responseCode = "400", description = "Bad Request"), + @ApiResponse(responseCode = "404", description = "Working Capital Loan not found") }) + public CommandProcessingResult createBreachAction( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + return createBreachAction(resolveExternalId(loanExternalId), apiRequestBodyAsJson); + } + + @GET + @Path("{loanId}/breach-actions") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve Breach Actions", description = "Retrieves all breach actions for a Working Capital loan") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = WorkingCapitalLoanBreachActionData.class)))) }) + public List retrieveBreachActions( + @PathParam("loanId") @Parameter(description = "loanId") final Long loanId) { + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + return readService.retrieveBreachActions(loanId); + } + + @GET + @Path("external-id/{loanExternalId}/breach-actions") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "retrieveBreachActionsByExternalId", summary = "Retrieve Breach Actions by external id", description = "Retrieves all breach actions for a Working Capital loan identified by external id") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = WorkingCapitalLoanBreachActionData.class)))) }) + public List retrieveBreachActions( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId) { + return retrieveBreachActions(resolveExternalId(loanExternalId)); + } + + private Long resolveExternalId(final String loanExternalIdStr) { + final ExternalId externalId = ExternalIdFactory.produce(loanExternalIdStr); + final Long resolvedLoanId = loanReadPlatformService.getResolvedLoanId(externalId); + if (resolvedLoanId == null) { + throw new WorkingCapitalLoanNotFoundException(externalId); + } + return resolvedLoanId; + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanBreachActionApiResourceSwagger.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanBreachActionApiResourceSwagger.java new file mode 100644 index 00000000000..117ba5b72ab --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanBreachActionApiResourceSwagger.java @@ -0,0 +1,64 @@ +/** + * 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.workingcapitalloan.api; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; + +public final class WorkingCapitalLoanBreachActionApiResourceSwagger { + + private WorkingCapitalLoanBreachActionApiResourceSwagger() {} + + @Schema(description = "PostWorkingCapitalLoansBreachActionRequest") + public static final class PostWorkingCapitalLoansBreachActionRequest { + + private PostWorkingCapitalLoansBreachActionRequest() {} + + @Schema(example = "reschedule", description = "Breach action type: reschedule") + public String action; + @Schema(example = "33.33", description = "Minimum payment value (required together with minimumPaymentType)") + public BigDecimal minimumPayment; + @Schema(example = "PERCENTAGE", description = "Minimum payment type: PERCENTAGE, FLAT (required together with minimumPayment)") + public String minimumPaymentType; + @Schema(example = "30", description = "Frequency value (required together with frequencyType)") + public Integer frequency; + @Schema(example = "DAYS", description = "Frequency type: DAYS, WEEKS, MONTHS, YEARS (required together with frequency)") + public String frequencyType; + @Schema(example = "yyyy-MM-dd") + public String dateFormat; + @Schema(example = "en") + public String locale; + } + + @Schema(description = "PostWorkingCapitalLoansBreachActionResponse") + public static final class PostWorkingCapitalLoansBreachActionResponse { + + private PostWorkingCapitalLoansBreachActionResponse() {} + + @Schema(example = "1") + public Long officeId; + + @Schema(example = "1") + public Long clientId; + + @Schema(example = "1") + public Long resourceId; + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanBreachActionData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanBreachActionData.java new file mode 100644 index 00000000000..fba96d752c7 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanBreachActionData.java @@ -0,0 +1,31 @@ +/** + * 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.workingcapitalloan.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPeriodFrequencyType; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalBreachAmountCalculationType; + +public record WorkingCapitalLoanBreachActionData(Long id, DelinquencyAction action, LocalDate startDate, LocalDate endDate, + BigDecimal minimumPayment, WorkingCapitalBreachAmountCalculationType minimumPaymentType, Integer frequency, + WorkingCapitalLoanPeriodFrequencyType frequencyType) { + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanBreachAction.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanBreachAction.java new file mode 100644 index 00000000000..0a0fa3eb200 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanBreachAction.java @@ -0,0 +1,73 @@ +/** + * 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.workingcapitalloan.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalBreachAmountCalculationType; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "m_wc_loan_breach_action") +public class WorkingCapitalLoanBreachAction extends AbstractAuditableWithUTCDateTimeCustom { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "wc_loan_id", nullable = false) + private WorkingCapitalLoan workingCapitalLoan; + + @Enumerated(EnumType.STRING) + @Column(name = "action", nullable = false) + private DelinquencyAction action; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + @Column(name = "minimum_payment", scale = 6, precision = 19) + private BigDecimal minimumPayment; + + @Enumerated(EnumType.STRING) + @Column(name = "minimum_payment_type") + private WorkingCapitalBreachAmountCalculationType minimumPaymentType; + + @Column(name = "frequency") + private Integer frequency; + + @Enumerated(EnumType.STRING) + @Column(name = "frequency_type") + private WorkingCapitalLoanPeriodFrequencyType frequencyType; + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/CreateWorkingCapitalLoanBreachActionCommandHandler.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/CreateWorkingCapitalLoanBreachActionCommandHandler.java new file mode 100644 index 00000000000..1ab7d124c9b --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/CreateWorkingCapitalLoanBreachActionCommandHandler.java @@ -0,0 +1,43 @@ +/** + * 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.workingcapitalloan.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanBreachActionWriteService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "WC_BREACH_ACTION", action = "CREATE") +public class CreateWorkingCapitalLoanBreachActionCommandHandler implements NewCommandSourceHandler { + + private final WorkingCapitalLoanBreachActionWriteService writeService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return writeService.createBreachAction(command.entityId(), command); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachActionRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachActionRepository.java new file mode 100644 index 00000000000..d2cec8de334 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachActionRepository.java @@ -0,0 +1,34 @@ +/** + * 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.workingcapitalloan.repository; + +import java.util.List; +import java.util.Optional; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachAction; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface WorkingCapitalLoanBreachActionRepository extends JpaRepository { + + List findByWorkingCapitalLoanIdOrderById(Long workingCapitalLoanId); + + Optional findTopByWorkingCapitalLoanIdAndActionOrderByIdDesc(Long workingCapitalLoanId, + DelinquencyAction action); + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachScheduleRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachScheduleRepository.java index e36098ea2e7..c53ccd21d5c 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachScheduleRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachScheduleRepository.java @@ -34,4 +34,6 @@ public interface WorkingCapitalLoanBreachScheduleRepository extends JpaRepositor Optional findByLoanIdAndFromDateLessThanEqualAndToDateGreaterThanEqual(Long loanId, LocalDate transactionDate, LocalDate transactionDate1); + + List findByLoanIdAndToDateBeforeAndBreach(Long loanId, LocalDate toDateBefore, Boolean breach); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionReadService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionReadService.java new file mode 100644 index 00000000000..955b12b3e65 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionReadService.java @@ -0,0 +1,28 @@ +/** + * 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.workingcapitalloan.service; + +import java.util.List; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanBreachActionData; + +public interface WorkingCapitalLoanBreachActionReadService { + + List retrieveBreachActions(Long workingCapitalLoanId); + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionReadServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionReadServiceImpl.java new file mode 100644 index 00000000000..8e8a59e69c4 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionReadServiceImpl.java @@ -0,0 +1,52 @@ +/** + * 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.workingcapitalloan.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanBreachActionData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachAction; +import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachActionRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class WorkingCapitalLoanBreachActionReadServiceImpl implements WorkingCapitalLoanBreachActionReadService { + + private final WorkingCapitalLoanBreachActionRepository actionRepository; + private final WorkingCapitalLoanRepository loanRepository; + + @Transactional(readOnly = true) + @Override + public List retrieveBreachActions(final Long workingCapitalLoanId) { + if (!loanRepository.existsById(workingCapitalLoanId)) { + throw new WorkingCapitalLoanNotFoundException(workingCapitalLoanId); + } + return actionRepository.findByWorkingCapitalLoanIdOrderById(workingCapitalLoanId).stream().map(this::toData).toList(); + } + + private WorkingCapitalLoanBreachActionData toData(final WorkingCapitalLoanBreachAction action) { + return new WorkingCapitalLoanBreachActionData(action.getId(), action.getAction(), action.getStartDate(), action.getEndDate(), + action.getMinimumPayment(), action.getMinimumPaymentType(), action.getFrequency(), action.getFrequencyType()); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionWriteService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionWriteService.java new file mode 100644 index 00000000000..13423b77529 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionWriteService.java @@ -0,0 +1,28 @@ +/** + * 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.workingcapitalloan.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface WorkingCapitalLoanBreachActionWriteService { + + CommandProcessingResult createBreachAction(Long workingCapitalLoanId, JsonCommand command); + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionWriteServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionWriteServiceImpl.java new file mode 100644 index 00000000000..2b0c4c0cbd7 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionWriteServiceImpl.java @@ -0,0 +1,71 @@ +/** + * 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.workingcapitalloan.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachAction; +import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachActionRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; +import org.apache.fineract.portfolio.workingcapitalloan.validator.WorkingCapitalLoanBreachActionParseAndValidator; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class WorkingCapitalLoanBreachActionWriteServiceImpl implements WorkingCapitalLoanBreachActionWriteService { + + private final WorkingCapitalLoanRepository loanRepository; + private final WorkingCapitalLoanBreachActionRepository actionRepository; + private final WorkingCapitalLoanBreachActionParseAndValidator validator; + private final WorkingCapitalLoanBreachScheduleService breachScheduleService; + + @Transactional + @Override + public CommandProcessingResult createBreachAction(final Long workingCapitalLoanId, final JsonCommand command) { + final WorkingCapitalLoan workingCapitalLoan = loanRepository.findById(workingCapitalLoanId) + .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(workingCapitalLoanId)); + + final WorkingCapitalLoanBreachAction action = validator.validateAndParse(command, workingCapitalLoan); + action.setWorkingCapitalLoan(workingCapitalLoan); + + final WorkingCapitalLoanBreachAction saved = actionRepository.saveAndFlush(action); + log.debug("Created WC loan breach action {} for loan {}", action.getAction(), workingCapitalLoanId); + + if (DelinquencyAction.RESCHEDULE.equals(action.getAction())) { + breachScheduleService.rescheduleMinimumPayment(workingCapitalLoan, action); + } + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(saved.getId()) // + .withLoanId(workingCapitalLoanId) // + .withOfficeId(workingCapitalLoan.getOfficeId()) // + .withClientId(workingCapitalLoan.getClientId()) // + .build(); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java index 67ebee0740a..f304387523d 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java @@ -23,6 +23,7 @@ import java.util.List; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanBreachScheduleData; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachAction; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachSchedule; public interface WorkingCapitalLoanBreachScheduleService { @@ -40,4 +41,6 @@ public interface WorkingCapitalLoanBreachScheduleService { void applyRepayment(Long loanId, LocalDate transactionDate, BigDecimal amount); void evaluateBreach(WorkingCapitalLoan loan, LocalDate businessDate); + + void rescheduleMinimumPayment(WorkingCapitalLoan loan, WorkingCapitalLoanBreachAction rescheduleAction); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java index b57b257cbbb..38e656d2f2d 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java @@ -26,15 +26,19 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanBreachScheduleData; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachAction; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachSchedule; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPeriodFrequencyType; import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.mapper.WorkingCapitalLoanBreachScheduleMapper; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachActionRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachScheduleRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; import org.apache.fineract.portfolio.workingcapitalloanbreach.domain.WorkingCapitalBreach; @@ -50,6 +54,7 @@ public class WorkingCapitalLoanBreachScheduleServiceImpl implements WorkingCapit private final WorkingCapitalLoanBreachScheduleRepository repository; private final WorkingCapitalLoanBreachScheduleMapper mapper; private final WorkingCapitalLoanRepository loanRepository; + private final WorkingCapitalLoanBreachActionRepository breachActionRepository; @Override public void generateInitialPeriod(final WorkingCapitalLoan loan) { @@ -66,8 +71,11 @@ public void generateInitialPeriod(final WorkingCapitalLoan loan) { final LocalDate fromDate = disbursementDateOptional.get().plusDays(getBreachGraceDays(loan)); final WorkingCapitalBreach breach = breachOpt.get(); - final LocalDate toDate = calculateToDate(fromDate, breach.getBreachFrequency(), breach.getBreachFrequencyType()); - final BigDecimal minPaymentAmount = calculateMinPaymentAmount(loan, breach); + final Optional latestReschedule = findLatestRescheduleAction(loan.getId()); + final Integer effectiveFrequency = resolveFrequency(latestReschedule.orElse(null), breach); + final WorkingCapitalLoanPeriodFrequencyType effectiveFreqType = resolveFrequencyType(latestReschedule.orElse(null), breach); + final LocalDate toDate = calculateToDate(fromDate, effectiveFrequency, effectiveFreqType); + final BigDecimal minPaymentAmount = calculateMinPaymentAmount(loan, breach, latestReschedule.orElse(null)); final WorkingCapitalLoanBreachSchedule period = createPeriod(loan, 1, fromDate, toDate, minPaymentAmount); repository.saveAndFlush(period); @@ -92,13 +100,16 @@ public void generateNextPeriodIfNeeded(final WorkingCapitalLoan loan, final Loca } final WorkingCapitalBreach breach = breachOpt.get(); - final BigDecimal minPaymentAmount = calculateMinPaymentAmount(loan, breach); + final Optional latestReschedule = findLatestRescheduleAction(loan.getId()); + final Integer effectiveFrequency = resolveFrequency(latestReschedule.orElse(null), breach); + final WorkingCapitalLoanPeriodFrequencyType effectiveFreqType = resolveFrequencyType(latestReschedule.orElse(null), breach); + final BigDecimal minPaymentAmount = calculateMinPaymentAmount(loan, breach, latestReschedule.orElse(null)); final List newPeriods = new ArrayList<>(); WorkingCapitalLoanBreachSchedule latestPeriod = latestPeriodOpt.get(); while (!latestPeriod.getToDate().isAfter(businessDate)) { final LocalDate newFromDate = latestPeriod.getToDate().plusDays(1); - final LocalDate newToDate = calculateToDate(newFromDate, breach.getBreachFrequency(), breach.getBreachFrequencyType()); + final LocalDate newToDate = calculateToDate(newFromDate, effectiveFrequency, effectiveFreqType); final WorkingCapitalLoanBreachSchedule nextPeriod = createPeriod(loan, latestPeriod.getPeriodNumber() + 1, newFromDate, newToDate, minPaymentAmount); @@ -170,6 +181,52 @@ public List retrieveBreachSchedule(final L return mapper.toDataList(periods); } + @Override + public void rescheduleMinimumPayment(final WorkingCapitalLoan loan, final WorkingCapitalLoanBreachAction rescheduleAction) { + final LocalDate businessDate = DateUtils.getBusinessLocalDate(); + final Optional breachOpt = getBreachConfig(loan); + if (breachOpt.isEmpty()) { + log.warn("No breach configuration found for WC loan {}, skipping reschedule", loan.getId()); + return; + } + final WorkingCapitalBreach breach = breachOpt.get(); + final BigDecimal newMinPaymentAmount = calculateMinPaymentAmount(loan, breach, rescheduleAction); + final Integer newFrequency = resolveFrequency(rescheduleAction, breach); + final WorkingCapitalLoanPeriodFrequencyType newFreqType = resolveFrequencyType(rescheduleAction, breach); + + final List periods = repository.findByLoanIdOrderByPeriodNumberAsc(loan.getId()); + + WorkingCapitalLoanBreachSchedule currentPeriod = null; + final List futurePeriods = new ArrayList<>(); + + for (final WorkingCapitalLoanBreachSchedule period : periods) { + if (period.getBreach() != null) { + continue; + } + final boolean isCurrent = !period.getFromDate().isAfter(businessDate) && !period.getToDate().isBefore(businessDate); + final boolean isFuture = period.getFromDate().isAfter(businessDate); + + if (isCurrent) { + currentPeriod = period; + period.setMinPaymentAmount(newMinPaymentAmount); + period.setOutstandingAmount(newMinPaymentAmount.subtract(period.getPaidAmount()).max(BigDecimal.ZERO)); + period.setNearBreach(null); + } else if (isFuture) { + futurePeriods.add(period); + } + } + + if (currentPeriod != null) { + repository.saveAndFlush(currentPeriod); + updateFuturePeriods(currentPeriod, futurePeriods, newMinPaymentAmount, newFrequency, newFreqType); + } + + evaluateExpiredBreaches(loan, businessDate); + + log.debug("Rescheduled breach schedule for WC loan {}: new minimumPayment={} {}, frequency={} {}", loan.getId(), + rescheduleAction.getMinimumPayment(), rescheduleAction.getMinimumPaymentType(), newFrequency, newFreqType); + } + private WorkingCapitalLoanBreachSchedule createPeriod(final WorkingCapitalLoan loan, final int periodNumber, final LocalDate fromDate, final LocalDate toDate, final BigDecimal minPaymentAmount) { final int numberOfDays = (int) ChronoUnit.DAYS.between(fromDate, toDate) + 1; @@ -210,13 +267,16 @@ private LocalDate calculateToDate(final LocalDate fromDate, final Integer freque }; } - private BigDecimal calculateMinPaymentAmount(final WorkingCapitalLoan loan, final WorkingCapitalBreach breach) { - final BigDecimal breachAmount = breach.getBreachAmount(); - if (breachAmount == null) { + private BigDecimal calculateMinPaymentAmount(final WorkingCapitalLoan loan, final WorkingCapitalBreach breach, + final WorkingCapitalLoanBreachAction rescheduleOverride) { + final BigDecimal effectiveBreachAmount = resolveBreachAmount(rescheduleOverride, breach); + if (effectiveBreachAmount == null) { return BigDecimal.ZERO; } - if (WorkingCapitalBreachAmountCalculationType.FLAT.equals(breach.getBreachAmountCalculationType())) { - return breachAmount; + final WorkingCapitalBreachAmountCalculationType effectiveCalculationType = resolveBreachAmountCalculationType(rescheduleOverride, + breach); + if (WorkingCapitalBreachAmountCalculationType.FLAT.equals(effectiveCalculationType)) { + return effectiveBreachAmount; } final BigDecimal principal = loan.getApprovedPrincipal(); if (principal == null) { @@ -224,8 +284,80 @@ private BigDecimal calculateMinPaymentAmount(final WorkingCapitalLoan loan, fina } final BigDecimal discount = loan.getLoanProductRelatedDetails() != null ? loan.getLoanProductRelatedDetails().getDiscount() : null; final BigDecimal base = discount != null ? principal.add(discount) : principal; - final BigDecimal rawAmount = MathUtil.percentageOf(base, breachAmount, MoneyHelper.getMathContext()); + final BigDecimal rawAmount = MathUtil.percentageOf(base, effectiveBreachAmount, MoneyHelper.getMathContext()); return Money.of(loan.getLoanProductRelatedDetails().getCurrency(), rawAmount).getAmount(); } + private Optional findLatestRescheduleAction(final Long loanId) { + return breachActionRepository.findTopByWorkingCapitalLoanIdAndActionOrderByIdDesc(loanId, DelinquencyAction.RESCHEDULE); + } + + private Integer resolveFrequency(final WorkingCapitalLoanBreachAction rescheduleOverride, final WorkingCapitalBreach breach) { + if (rescheduleOverride != null && rescheduleOverride.getFrequency() != null) { + return rescheduleOverride.getFrequency(); + } + return breach.getBreachFrequency(); + } + + private WorkingCapitalLoanPeriodFrequencyType resolveFrequencyType(final WorkingCapitalLoanBreachAction rescheduleOverride, + final WorkingCapitalBreach breach) { + if (rescheduleOverride != null && rescheduleOverride.getFrequencyType() != null) { + return rescheduleOverride.getFrequencyType(); + } + return breach.getBreachFrequencyType(); + } + + private BigDecimal resolveBreachAmount(final WorkingCapitalLoanBreachAction rescheduleOverride, final WorkingCapitalBreach breach) { + if (rescheduleOverride != null && rescheduleOverride.getMinimumPayment() != null) { + return rescheduleOverride.getMinimumPayment(); + } + return breach.getBreachAmount(); + } + + private WorkingCapitalBreachAmountCalculationType resolveBreachAmountCalculationType( + final WorkingCapitalLoanBreachAction rescheduleOverride, final WorkingCapitalBreach breach) { + if (rescheduleOverride != null && rescheduleOverride.getMinimumPaymentType() != null) { + return rescheduleOverride.getMinimumPaymentType(); + } + return breach.getBreachAmountCalculationType() != null ? breach.getBreachAmountCalculationType() + : WorkingCapitalBreachAmountCalculationType.PERCENTAGE; + } + + private void evaluateExpiredBreaches(final WorkingCapitalLoan loan, final LocalDate businessDate) { + final List periods = repository.findByLoanIdOrderByPeriodNumberAsc(loan.getId()); + for (final WorkingCapitalLoanBreachSchedule period : periods) { + if (period.getBreach() != null) { + continue; + } + if (!period.getToDate().isAfter(businessDate) && evaluateBreachOnDate(period, businessDate)) { + repository.saveAndFlush(period); + } + } + } + + private void updateFuturePeriods(final WorkingCapitalLoanBreachSchedule currentPeriod, + final List existingFuturePeriods, final BigDecimal minPaymentAmount, final Integer frequency, + final WorkingCapitalLoanPeriodFrequencyType frequencyType) { + int periodNumber = currentPeriod.getPeriodNumber(); + LocalDate fromDate = currentPeriod.getToDate().plusDays(1); + + for (final WorkingCapitalLoanBreachSchedule period : existingFuturePeriods) { + final LocalDate toDate = calculateToDate(fromDate, frequency, frequencyType); + periodNumber++; + + period.setPeriodNumber(periodNumber); + period.setFromDate(fromDate); + period.setToDate(toDate); + period.setNumberOfDays((int) ChronoUnit.DAYS.between(fromDate, toDate) + 1); + period.setMinPaymentAmount(minPaymentAmount); + period.setPaidAmount(BigDecimal.ZERO); + period.setOutstandingAmount(minPaymentAmount); + period.setNearBreach(null); + period.setBreach(null); + + fromDate = toDate.plusDays(1); + } + repository.saveAll(existingFuturePeriods); + } + } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/validator/WorkingCapitalLoanBreachActionParseAndValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/validator/WorkingCapitalLoanBreachActionParseAndValidator.java new file mode 100644 index 00000000000..03c3558a817 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/validator/WorkingCapitalLoanBreachActionParseAndValidator.java @@ -0,0 +1,205 @@ +/** + * 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.workingcapitalloan.validator; + +import static org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParameters.ACTION; + +import com.google.gson.JsonElement; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.validator.ParseAndValidator; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachAction; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachSchedule; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDisbursementDetails; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPeriodFrequencyType; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachScheduleRepository; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalBreachAmountCalculationType; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class WorkingCapitalLoanBreachActionParseAndValidator extends ParseAndValidator { + + private static final String RESCHEDULE_ACTION = "reschedule"; + private static final String MINIMUM_PAYMENT = "minimumPayment"; + private static final String MINIMUM_PAYMENT_TYPE = "minimumPaymentType"; + private static final String FREQUENCY = "frequency"; + private static final String FREQUENCY_TYPE = "frequencyType"; + + private final FromJsonHelper jsonHelper; + private final WorkingCapitalLoanBreachScheduleRepository breachScheduleRepository; + + public WorkingCapitalLoanBreachAction validateAndParse(final JsonCommand command, final WorkingCapitalLoan workingCapitalLoan) { + final DataValidatorBuilder dataValidator = new DataValidatorBuilder(new ArrayList<>()).resource("workingCapitalLoanBreachAction"); + final WorkingCapitalLoanBreachAction parsedAction = parseCommand(command, dataValidator); + validateLoanIsActive(workingCapitalLoan, dataValidator); + + if (DelinquencyAction.RESCHEDULE.equals(parsedAction.getAction())) { + validateReschedule(parsedAction, workingCapitalLoan, dataValidator); + } else if (parsedAction.getAction() != null) { + dataValidator.reset().parameter(ACTION).value(parsedAction.getAction()).failWithCode("invalid.action"); + } + + throwExceptionIfValidationWarningsExist(dataValidator); + return parsedAction; + } + + private WorkingCapitalLoanBreachAction parseCommand(final JsonCommand command, final DataValidatorBuilder dataValidator) { + final JsonElement json = command.parsedJson(); + final WorkingCapitalLoanBreachAction action = new WorkingCapitalLoanBreachAction(); + action.setAction(extractAction(json, dataValidator)); + action.setStartDate(DateUtils.getBusinessLocalDate()); + action.setMinimumPayment(extractBigDecimal(json, MINIMUM_PAYMENT)); + action.setMinimumPaymentType(extractMinimumPaymentType(json, dataValidator)); + action.setFrequency(extractInteger(json, FREQUENCY)); + action.setFrequencyType(extractFrequencyType(json, dataValidator)); + return action; + } + + private DelinquencyAction extractAction(final JsonElement json, final DataValidatorBuilder dataValidator) { + final String actionString = jsonHelper.extractStringNamed(ACTION, json); + dataValidator.reset().parameter(ACTION).value(actionString).notBlank(); + if (StringUtils.isNotBlank(actionString)) { + dataValidator.reset().parameter(ACTION).value(actionString).isOneOfTheseStringValues(RESCHEDULE_ACTION); + } + if (RESCHEDULE_ACTION.equalsIgnoreCase(actionString)) { + return DelinquencyAction.RESCHEDULE; + } + return null; + } + + private BigDecimal extractBigDecimal(final JsonElement json, final String paramName) { + if (json.getAsJsonObject().has(paramName)) { + return jsonHelper.extractBigDecimalWithLocaleNamed(paramName, json); + } + return null; + } + + private Integer extractInteger(final JsonElement json, final String paramName) { + if (json.getAsJsonObject().has(paramName)) { + return jsonHelper.extractIntegerWithLocaleNamed(paramName, json); + } + return null; + } + + private WorkingCapitalBreachAmountCalculationType extractMinimumPaymentType(final JsonElement json, + final DataValidatorBuilder dataValidator) { + final String value = jsonHelper.extractStringNamed(MINIMUM_PAYMENT_TYPE, json); + if (StringUtils.isEmpty(value)) { + return null; + } + try { + return WorkingCapitalBreachAmountCalculationType.valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + dataValidator.reset().parameter(MINIMUM_PAYMENT_TYPE).value(value).failWithCode("invalid.minimumPaymentType"); + return null; + } + } + + private WorkingCapitalLoanPeriodFrequencyType extractFrequencyType(final JsonElement json, final DataValidatorBuilder dataValidator) { + final String value = jsonHelper.extractStringNamed(FREQUENCY_TYPE, json); + if (StringUtils.isEmpty(value)) { + return null; + } + try { + return WorkingCapitalLoanPeriodFrequencyType.valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + dataValidator.reset().parameter(FREQUENCY_TYPE).value(value).failWithCode("invalid.frequencyType"); + return null; + } + } + + private void validateReschedule(final WorkingCapitalLoanBreachAction action, final WorkingCapitalLoan workingCapitalLoan, + final DataValidatorBuilder dataValidator) { + validateLoanIsDisbursed(workingCapitalLoan, dataValidator); + validateScheduleExists(workingCapitalLoan, dataValidator); + validateBreachConfigured(workingCapitalLoan, dataValidator); + + final boolean hasPaymentGroup = action.getMinimumPayment() != null || action.getMinimumPaymentType() != null; + final boolean hasFrequencyGroup = action.getFrequency() != null || action.getFrequencyType() != null; + + if (!hasPaymentGroup && !hasFrequencyGroup) { + dataValidator.reset().failWithCodeNoParameterAddedToErrorCode("reschedule.no.change.parameters"); + } + if (hasPaymentGroup) { + validateMinimumPaymentGroupProvided(action, dataValidator); + } + if (hasFrequencyGroup) { + validateFrequencyGroupProvided(action, dataValidator); + } + } + + private void validateLoanIsActive(final WorkingCapitalLoan workingCapitalLoan, final DataValidatorBuilder dataValidator) { + if (!workingCapitalLoan.getLoanStatus().isActive()) { + dataValidator.reset().failWithCodeNoParameterAddedToErrorCode("loan.is.not.active"); + } + } + + private void validateLoanIsDisbursed(final WorkingCapitalLoan workingCapitalLoan, final DataValidatorBuilder dataValidator) { + final boolean isDisbursed = workingCapitalLoan.getDisbursementDetails().stream() + .map(WorkingCapitalLoanDisbursementDetails::getActualDisbursementDate).anyMatch(Objects::nonNull); + if (!isDisbursed) { + dataValidator.reset().failWithCodeNoParameterAddedToErrorCode("loan.not.disbursed"); + } + } + + private void validateScheduleExists(final WorkingCapitalLoan workingCapitalLoan, final DataValidatorBuilder dataValidator) { + final List periods = breachScheduleRepository + .findByLoanIdOrderByPeriodNumberAsc(workingCapitalLoan.getId()); + if (periods.isEmpty()) { + dataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.breach.schedule"); + } + } + + private void validateBreachConfigured(final WorkingCapitalLoan workingCapitalLoan, final DataValidatorBuilder dataValidator) { + if (workingCapitalLoan.getLoanProductRelatedDetails() == null + || workingCapitalLoan.getLoanProductRelatedDetails().getBreach() == null) { + dataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.breach.configuration"); + } + } + + private void validateMinimumPaymentGroupProvided(final WorkingCapitalLoanBreachAction action, + final DataValidatorBuilder dataValidator) { + if (action.getMinimumPayment() == null || action.getMinimumPayment().compareTo(BigDecimal.ZERO) <= 0) { + dataValidator.reset().parameter(MINIMUM_PAYMENT).value(action.getMinimumPayment()).failWithCode("must.be.greater.than.zero"); + } + if (action.getMinimumPaymentType() == null) { + dataValidator.reset().parameter(MINIMUM_PAYMENT_TYPE).value(action.getMinimumPaymentType()).notNull(); + } + } + + private void validateFrequencyGroupProvided(final WorkingCapitalLoanBreachAction action, final DataValidatorBuilder dataValidator) { + if (action.getFrequency() == null || action.getFrequency() <= 0) { + dataValidator.reset().parameter(FREQUENCY).value(action.getFrequency()).integerGreaterThanZero(); + } + if (action.getFrequencyType() == null) { + dataValidator.reset().parameter(FREQUENCY_TYPE).value(action.getFrequencyType()).notNull(); + } + } +} 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 6fcc1ddd652..a37befc63da 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 @@ -66,4 +66,6 @@ + + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0045_wc_loan_breach_action.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0045_wc_loan_breach_action.xml new file mode 100644 index 00000000000..12acc34a050 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0045_wc_loan_breach_action.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_WC_BREACH_ACTION' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'READ_WC_BREACH_ACTION' + + + + + + + + + + + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0046_wc_loan_breach_action_reschedule.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0046_wc_loan_breach_action_reschedule.xml new file mode 100644 index 00000000000..524b48a9979 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0046_wc_loan_breach_action_reschedule.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml b/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml index 17fa19e7760..4b4993d4975 100644 --- a/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml +++ b/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml @@ -162,6 +162,7 @@ org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyAction + org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachAction org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachSchedule org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeSchedule org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeScheduleTagHistory From 54bb535412e55a6dea16553cd390307fe178af9d Mon Sep 17 00:00:00 2001 From: Peter Kovacs Date: Fri, 19 Jun 2026 16:04:23 +0200 Subject: [PATCH 2/2] FINERACT-2455: WC - Breach Management - Recalculation when Breach evaluation is changed(breach reschedule) - E2E tests --- .../WorkingCapitalBreachActionStepDef.java | 41 +---- .../WorkingCapitalBreachReschedule.feature | 157 ++++++++++-------- 2 files changed, 87 insertions(+), 111 deletions(-) diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalBreachActionStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalBreachActionStepDef.java index 48535070504..1dcda7b909a 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalBreachActionStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalBreachActionStepDef.java @@ -41,7 +41,6 @@ import org.apache.fineract.client.models.PostWorkingCapitalLoansBreachActionResponse; import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse; import org.apache.fineract.client.models.WorkingCapitalLoanBreachActionData; -import org.apache.fineract.client.models.WorkingCapitalLoanBreachScheduleData; import org.apache.fineract.test.factory.WorkingCapitalLoanRequestFactory; import org.apache.fineract.test.stepdef.AbstractStepDef; import org.apache.fineract.test.support.TestContextKey; @@ -99,26 +98,9 @@ public void verifyBreachActionsHistory(final DataTable table) { log.info("Successfully verified {} breach action(s) for loan {}", actions.size(), loanId); } - @Then("Working Capital loan breach schedule periods have specific data:") - public void verifySpecificPeriods(final DataTable table) { - final Long loanId = getLoanId(); - final List periods = ok( - () -> fineractFeignClient.workingCapitalLoanBreachSchedule().retrieveBreachSchedule(loanId)); - - for (final Map expected : table.asMaps()) { - final int periodNumber = Integer.parseInt(expected.get("periodNumber")); - final WorkingCapitalLoanBreachScheduleData actual = periods.stream().filter(p -> { - assert p.getPeriodNumber() != null; - return p.getPeriodNumber().equals(periodNumber); - }).findFirst().orElse(null); - assertThat(actual).as("Period %d should exist", periodNumber).isNotNull(); - expected.forEach((field, value) -> verifyScheduleField(actual, field, value, periodNumber)); - } - } - private void executeRescheduleAction(final PostWorkingCapitalLoansBreachActionRequest request) { final Long loanId = getLoanId(); - log.info("Creating breach RESCHEDULE action for WC loan {}: {}", loanId, request); + log.debug("Creating breach RESCHEDULE action for WC loan {}: {}", loanId, request); final PostWorkingCapitalLoansBreachActionResponse result = ok( () -> fineractFeignClient.workingCapitalLoanBreachActions().createBreachAction(loanId, request)); @@ -152,27 +134,6 @@ private void verifyActionField(final WorkingCapitalLoanBreachActionData actual, } } - private void verifyScheduleField(final WorkingCapitalLoanBreachScheduleData actual, final String field, final String expected, - final int periodNumber) { - final String label = "Period " + periodNumber + " " + field; - switch (field) { - case "periodNumber" -> assertThat(actual.getPeriodNumber()).as(label).isEqualTo(Integer.parseInt(expected)); - case "fromDate" -> assertThat(actual.getFromDate()).as(label).isEqualTo(LocalDate.parse(expected)); - case "toDate" -> assertThat(actual.getToDate()).as(label).isEqualTo(LocalDate.parse(expected)); - case "numberOfDays" -> - verifyOptionalField(expected, v -> assertThat(actual.getNumberOfDays()).as(label).isEqualTo(Integer.parseInt(v)), - () -> assertThat(actual.getNumberOfDays()).as(label).isNull()); - case "minPaymentAmount" -> assertThat(actual.getMinPaymentAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)); - case "outstandingAmount" -> assertThat(actual.getOutstandingAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)); - case "nearBreach" -> - verifyOptionalField(expected, v -> assertThat(actual.getNearBreach()).as(label).isEqualTo(Boolean.parseBoolean(v)), - () -> assertThat(actual.getNearBreach()).as(label).isNull()); - case "breach" -> verifyOptionalField(expected, v -> assertThat(actual.getBreach()).as(label).isEqualTo(Boolean.parseBoolean(v)), - () -> assertThat(actual.getBreach()).as(label).isNull()); - default -> throw new IllegalArgumentException("Unknown schedule field: " + field); - } - } - private void verifyOptionalField(final String expected, final Consumer whenPresent, final Runnable whenAbsent) { Optional.ofNullable(expected).filter(Predicate.not(String::isBlank)).ifPresentOrElse(whenPresent, whenAbsent); } diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachReschedule.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachReschedule.feature index cbe7ba2ce59..737c7366c20 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachReschedule.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachReschedule.feature @@ -2,7 +2,8 @@ @WorkingCapitalBreachRescheduleActionFeature @WCCOBFeature Feature: Working Capital Breach Reschedule Action - Scenario: Verify that breach reschedule changes minimumPayment only + @TestRailId:C85272 + Scenario: Verify breach reschedule - UC1: changes minimumPayment only When Admin sets the business date to "01 January 2026" And Admin creates a client with random data And Admin creates a new Working Capital Loan Product with breachId and overrides enabled @@ -24,14 +25,15 @@ Feature: Working Capital Breach Reschedule Action | 1 | PERCENTAGE | When Admin sets the business date to "15 August 2026" And Admin runs inline COB job for Working Capital Loan by loanId - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2026-01-01 | 2026-02-28 | 110.70 | 110.70 | true | - | 2 | 2026-03-01 | 2026-04-30 | 110.70 | 110.70 | true | - | 3 | 2026-05-01 | 2026-06-30 | 90 | 90 | true | - | 4 | 2026-07-01 | 2026-08-31 | 90 | 90 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-02-28 | 59 | 110.70 | 110.70 | null | true | + | 2 | 2026-03-01 | 2026-04-30 | 61 | 110.70 | 110.70 | null | true | + | 3 | 2026-05-01 | 2026-06-30 | 61 | 90 | 90 | null | true | + | 4 | 2026-07-01 | 2026-08-31 | 62 | 90 | 90 | null | null | - Scenario: Verify that breach reschedule changes frequency only + @TestRailId:C85273 + Scenario: Verify breach reschedule - UC2: changes frequency only When Admin sets the business date to "01 January 2026" And Admin creates a client with random data And Admin creates a new Working Capital Loan Product with breachId and overrides enabled @@ -48,15 +50,16 @@ Feature: Working Capital Breach Reschedule Action | 30 | DAYS | When Admin sets the business date to "15 August 2026" And Admin runs inline COB job for Working Capital Loan by loanId - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2026-01-01 | 2026-02-28 | 110.70 | 110.70 | true | - | 2 | 2026-03-01 | 2026-04-30 | 110.70 | 110.70 | true | - | 3 | 2026-05-01 | 2026-06-30 | 110.70 | 110.70 | true | - | 4 | 2026-07-01 | 2026-07-30 | 110.70 | 110.70 | true | - | 5 | 2026-07-31 | 2026-08-29 | 110.70 | 110.70 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-02-28 | 59 | 110.70 | 110.70 | null | true | + | 2 | 2026-03-01 | 2026-04-30 | 61 | 110.70 | 110.70 | null | true | + | 3 | 2026-05-01 | 2026-06-30 | 61 | 110.70 | 110.70 | null | true | + | 4 | 2026-07-01 | 2026-07-30 | 30 | 110.70 | 110.70 | null | true | + | 5 | 2026-07-31 | 2026-08-29 | 30 | 110.70 | 110.70 | null | null | - Scenario: Verify that breach reschedule changes minimumPayment and frequency + @TestRailId:C85274 + Scenario: Verify breach reschedule - UC3: changes minimumPayment and frequency When Admin sets the business date to "01 January 2026" And Admin creates a client with random data And Admin creates a new Working Capital Loan Product with breachId and overrides enabled @@ -73,15 +76,16 @@ Feature: Working Capital Breach Reschedule Action | 1 | PERCENTAGE | 30 | DAYS | When Admin sets the business date to "15 August 2026" And Admin runs inline COB job for Working Capital Loan by loanId - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2026-01-01 | 2026-02-28 | 110.70 | 110.70 | true | - | 2 | 2026-03-01 | 2026-04-30 | 110.70 | 110.70 | true | - | 3 | 2026-05-01 | 2026-06-30 | 90 | 90 | true | - | 4 | 2026-07-01 | 2026-07-30 | 90 | 90 | true | - | 5 | 2026-07-31 | 2026-08-29 | 90 | 90 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-02-28 | 59 | 110.70 | 110.70 | null | true | + | 2 | 2026-03-01 | 2026-04-30 | 61 | 110.70 | 110.70 | null | true | + | 3 | 2026-05-01 | 2026-06-30 | 61 | 90 | 90 | null | true | + | 4 | 2026-07-01 | 2026-07-30 | 30 | 90 | 90 | null | true | + | 5 | 2026-07-31 | 2026-08-29 | 30 | 90 | 90 | null | null | - Scenario: Verify that the latest breach reschedule action wins + @TestRailId:C85275 + Scenario: Verify breach reschedule - UC4: latest reschedule action wins When Admin sets the business date to "01 January 2026" And Admin creates a client with random data And Admin creates a new Working Capital Loan Product with breachId and overrides enabled @@ -101,12 +105,16 @@ Feature: Working Capital Breach Reschedule Action | 1 | PERCENTAGE | 30 | DAYS | When Admin sets the business date to "15 August 2026" And Admin runs inline COB job for Working Capital Loan by loanId - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 3 | 2026-05-01 | 2026-06-30 | 90 | 90 | true | - | 4 | 2026-07-01 | 2026-07-30 | 90 | 90 | true | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-02-28 | 59 | 110.70 | 110.70 | null | true | + | 2 | 2026-03-01 | 2026-04-30 | 61 | 110.70 | 110.70 | null | true | + | 3 | 2026-05-01 | 2026-06-30 | 61 | 90 | 90 | null | true | + | 4 | 2026-07-01 | 2026-07-30 | 30 | 90 | 90 | null | true | + | 5 | 2026-07-31 | 2026-08-29 | 30 | 90 | 90 | null | null | - Scenario: Verify multiple breach reschedules on the same date are stored in history + @TestRailId:C85276 + Scenario: Verify breach reschedule - UC5: multiple reschedules on the same date are stored in history When Admin sets the business date to "01 January 2026" And Admin creates a client with random data And Admin creates a new Working Capital Loan Product with breachId and overrides enabled @@ -133,7 +141,8 @@ Feature: Working Capital Breach Reschedule Action | RESCHEDULE | 01 June 2026 | 1 | PERCENTAGE | 2 | MONTHS | | RESCHEDULE | 01 June 2026 | 1.5 | PERCENTAGE | 2 | MONTHS | - Scenario: Verify breach reschedule fails when no change parameters are provided + @TestRailId:C85277 + Scenario: Verify breach reschedule - UC6: fails when no change parameters are provided (Negative) When Admin sets the business date to "01 January 2026" And Admin creates a client with random data And Admin creates a new Working Capital Loan Product with breachId and overrides enabled @@ -145,7 +154,8 @@ Feature: Working Capital Breach Reschedule Action And Admin runs inline COB job for Working Capital Loan by loanId Then Admin fails to create WC breach reschedule action with no parameters with error containing "reschedule.no.change.parameters" - Scenario: Verify breach reschedule fails with negative minimumPayment + @TestRailId:C85278 + Scenario: Verify breach reschedule - UC7: fails with negative minimumPayment (Negative) When Admin sets the business date to "01 January 2026" And Admin creates a client with random data And Admin creates a new Working Capital Loan Product with breachId and overrides enabled @@ -157,7 +167,8 @@ Feature: Working Capital Breach Reschedule Action And Admin runs inline COB job for Working Capital Loan by loanId Then Admin fails to create WC breach reschedule action with minimumPayment -1 PERCENTAGE and frequency 30 DAYS with error containing "minimumPayment" - Scenario: Verify that payment-only reschedule after frequency reschedule falls back to product breach frequency + @TestRailId:C85279 + Scenario: Verify breach reschedule - UC8: payment-only reschedule after frequency reschedule falls back to product breach frequency When Admin sets the business date to "01 January 2026" And Admin creates a client with random data And Admin creates a new Working Capital Loan Product with breachId and overrides enabled @@ -177,14 +188,15 @@ Feature: Working Capital Breach Reschedule Action | 1 | PERCENTAGE | When Admin sets the business date to "15 August 2026" And Admin runs inline COB job for Working Capital Loan by loanId - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2026-01-01 | 2026-02-28 | 110.70 | 110.70 | true | - | 2 | 2026-03-01 | 2026-04-30 | 110.70 | 110.70 | true | - | 3 | 2026-05-01 | 2026-06-30 | 90 | 90 | true | - | 4 | 2026-07-01 | 2026-08-31 | 90 | 90 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-02-28 | 59 | 110.70 | 110.70 | null | true | + | 2 | 2026-03-01 | 2026-04-30 | 61 | 110.70 | 110.70 | null | true | + | 3 | 2026-05-01 | 2026-06-30 | 61 | 90 | 90 | null | true | + | 4 | 2026-07-01 | 2026-08-31 | 62 | 90 | 90 | null | null | - Scenario: Verify breach reschedule updates current period after partial repayment and replays payments + @TestRailId:C85280 + Scenario: Verify breach reschedule - UC9: updates current period after partial repayment and replays payments When Admin sets the business date to "01 January 2019" And Admin creates a client with random data And Admin creates a Working Capital Loan Product with custom breach config and overrides enabled: @@ -202,11 +214,12 @@ Feature: Working Capital Breach Reschedule Action And Admin creates WC breach reschedule action with the following parameters: | minimumPayment | minimumPaymentType | | 5 | PERCENTAGE | - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2019-01-01 | 2019-03-31 | 500.00 | 50.00 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2019-01-01 | 2019-03-31 | 90 | 500.00 | 50.00 | null | null | - Scenario: Verify breach reschedule preserves already evaluated periods + @TestRailId:C85281 + Scenario: Verify breach reschedule - UC10: preserves already evaluated periods When Admin sets the business date to "01 January 2019" And Admin creates a client with random data And Admin creates a Working Capital Loan Product with custom breach config and overrides enabled: @@ -222,20 +235,21 @@ Feature: Working Capital Breach Reschedule Action And Customer makes repayment on "05 March 2019" with 450.0 transaction amount on Working Capital loan When Admin sets the business date to "06 April 2019" And Admin runs inline COB job for Working Capital Loan by loanId - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2019-01-01 | 2019-03-31 | 900.00 | 450.00 | true | - | 2 | 2019-04-01 | 2019-06-29 | 900.00 | 900.00 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2019-01-01 | 2019-03-31 | 90 | 900.00 | 450.00 | null | true | + | 2 | 2019-04-01 | 2019-06-29 | 90 | 900.00 | 900.00 | null | null | When Admin sets the business date to "10 April 2019" And Admin creates WC breach reschedule action with the following parameters: | minimumPayment | minimumPaymentType | | 5 | PERCENTAGE | - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2019-01-01 | 2019-03-31 | 900.00 | 450.00 | true | - | 2 | 2019-04-01 | 2019-06-29 | 450.00 | 450.00 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2019-01-01 | 2019-03-31 | 90 | 900.00 | 450.00 | null | true | + | 2 | 2019-04-01 | 2019-06-29 | 90 | 450.00 | 450.00 | null | null | - Scenario: Verify breach reschedule changes frequency from 90 days to 30 days for current and future periods + @TestRailId:C85282 + Scenario: Verify breach reschedule - UC11: changes frequency from 90 days to 30 days for current and future periods When Admin sets the business date to "01 January 2019" And Admin creates a client with random data And Admin creates a Working Capital Loan Product with custom breach config and overrides enabled: @@ -248,9 +262,9 @@ Feature: Working Capital Breach Reschedule Action And Admin successfully disburse the Working Capital loan on "01 January 2019" with "9000" EUR transaction amount and "1000" discount amount And Admin runs inline COB job for Working Capital Loan by loanId When Admin sets the business date to "10 March 2019" - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2019-01-01 | 2019-03-31 | 900.00 | 900.00 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2019-01-01 | 2019-03-31 | 90 | 900.00 | 900.00 | null | null | And Admin creates WC breach reschedule action with the following parameters: | frequency | frequencyType | | 30 | DAYS | @@ -258,14 +272,15 @@ Feature: Working Capital Breach Reschedule Action And Admin runs inline COB job for Working Capital Loan by loanId When Admin sets the business date to "15 June 2019" And Admin runs inline COB job for Working Capital Loan by loanId - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2019-01-01 | 2019-03-31 | 900.00 | 900.00 | true | - | 2 | 2019-04-01 | 2019-04-30 | 900.00 | 900.00 | true | - | 3 | 2019-05-01 | 2019-05-30 | 900.00 | 900.00 | true | - | 4 | 2019-05-31 | 2019-06-29 | 900.00 | 900.00 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2019-01-01 | 2019-03-31 | 90 | 900.00 | 900.00 | null | true | + | 2 | 2019-04-01 | 2019-04-30 | 30 | 900.00 | 900.00 | null | true | + | 3 | 2019-05-01 | 2019-05-30 | 30 | 900.00 | 900.00 | null | true | + | 4 | 2019-05-31 | 2019-06-29 | 30 | 900.00 | 900.00 | null | null | - Scenario: Verify breach reschedule changes minimum payment and frequency together + @TestRailId:C85283 + Scenario: Verify breach reschedule - UC12: changes minimum payment and frequency together When Admin sets the business date to "01 January 2019" And Admin creates a client with random data And Admin creates a Working Capital Loan Product with custom breach config and overrides enabled: @@ -278,9 +293,9 @@ Feature: Working Capital Breach Reschedule Action And Admin successfully disburse the Working Capital loan on "01 January 2019" with "9000" EUR transaction amount and "1000" discount amount And Admin runs inline COB job for Working Capital Loan by loanId When Admin sets the business date to "10 March 2019" - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2019-01-01 | 2019-03-31 | 900.00 | 900.00 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2019-01-01 | 2019-03-31 | 90 | 900.00 | 900.00 | null | null | And Admin creates WC breach reschedule action with the following parameters: | minimumPayment | minimumPaymentType | frequency | frequencyType | | 5 | PERCENTAGE | 30 | DAYS | @@ -288,9 +303,9 @@ Feature: Working Capital Breach Reschedule Action And Admin runs inline COB job for Working Capital Loan by loanId When Admin sets the business date to "15 June 2019" And Admin runs inline COB job for Working Capital Loan by loanId - Then Working Capital loan breach schedule periods have specific data: - | periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | breach | - | 1 | 2019-01-01 | 2019-03-31 | 500.00 | 500.00 | true | - | 2 | 2019-04-01 | 2019-04-30 | 500.00 | 500.00 | true | - | 3 | 2019-05-01 | 2019-05-30 | 500.00 | 500.00 | true | - | 4 | 2019-05-31 | 2019-06-29 | 500.00 | 500.00 | | + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2019-01-01 | 2019-03-31 | 90 | 500.00 | 500.00 | null | true | + | 2 | 2019-04-01 | 2019-04-30 | 30 | 500.00 | 500.00 | null | true | + | 3 | 2019-05-01 | 2019-05-30 | 30 | 500.00 | 500.00 | null | true | + | 4 | 2019-05-31 | 2019-06-29 | 30 | 500.00 | 500.00 | null | null |