From a41da88940da6d6e31ea8d4c03060a578c250f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soma=20S=C3=B6r=C3=B6s?= Date: Thu, 26 Mar 2026 14:14:18 +0100 Subject: [PATCH 1/2] FINERACT-2455: WC - Delinquency Management - Delinquency days & Delinquency Bucket handling - create SetWorkingCapitalLoanDelinquencyTagsBusinessStep - create ENABLE_INSTANT_DELINQUENCY_CALCULATION - Delinquency Classification for Working Capital Loans --- .../api/GlobalConfigurationConstants.java | 1 + .../stepdef/loan/WorkingCapitalStepDef.java | 39 +++++ .../WorkingCapitalDelinquency.feature | 101 ++++++++++++ .../features/WorkingCapital_COB.feature | 7 +- ...DelinquencyClassificationBusinessStep.java | 86 +++++++++++ ...InternalWorkingCapitalLoanApiResource.java | 20 +++ .../api/WorkingCapitalLoanApiResource.java | 40 +++++ .../WorkingCapitalLoanApiResourceSwagger.java | 81 ++++++++++ ...ernalWorkingCapitalLoanPaymentRequest.java | 31 ++++ .../WorkingCapitalLoanCollectionData.java | 49 ++++++ .../data/WorkingCapitalLoanData.java | 2 + ...gCapitalLoanDelinquencyTagHistoryData.java | 65 ++++++++ ...pitalLoanRangeScheduleDelinquencyData.java | 38 +++++ ...oanDelinquencyRangeScheduleTagHistory.java | 77 +++++++++ ...inquencyRangeScheduleTagHistoryMapper.java | 58 +++++++ .../mapper/WorkingCapitalLoanMapper.java | 1 + ...oanDelinquencyRangeScheduleRepository.java | 9 ++ ...encyRangeScheduleTagHistoryRepository.java | 53 +++++++ ...ernalWorkingCapitalLoanPaymentService.java | 27 ++++ ...lWorkingCapitalLoanPaymentServiceImpl.java | 55 +++++++ ...oanApplicationReadPlatformServiceImpl.java | 15 +- ...lLoanDelinquencyClassificationService.java | 29 ++++ ...nDelinquencyClassificationServiceImpl.java | 144 +++++++++++++++++ ...anDelinquencyRangeScheduleServiceImpl.java | 30 +++- ...talLoanDelinquencyReadPlatformService.java | 32 ++++ ...oanDelinquencyReadPlatformServiceImpl.java | 82 ++++++++++ .../module-changelog-master.xml | 1 + .../0024_wc_loan_delinquincy_service.xml | 146 ++++++++++++++++++ .../persistence.xml | 1 + .../GlobalConfigurationTest.java | 43 ++++-- .../common/GlobalConfigurationHelper.java | 7 + 31 files changed, 1349 insertions(+), 21 deletions(-) create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/WorkingCapitalLoanDelinquencyClassificationBusinessStep.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/InternalWorkingCapitalLoanPaymentRequest.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanCollectionData.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanDelinquencyTagHistoryData.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanRangeScheduleDelinquencyData.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanDelinquencyRangeScheduleTagHistory.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentService.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentServiceImpl.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformService.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0024_wc_loan_delinquincy_service.xml diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java index 7117f5d8125..9daa57a89a0 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java @@ -81,6 +81,7 @@ public final class GlobalConfigurationConstants { public static final String ALLOWED_LOAN_STATUSES_OF_DELAYED_SETTLEMENT_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-of-delayed-settlement-for-external-asset-transfer"; public static final String MAX_LOGIN_RETRY_ATTEMPTS = "max-login-retry-attempts"; public static final String ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION = "enable-originator-creation-during-loan-application"; + public static final String ENABLE_INSTANT_DELINQUENCY_CALCULATION = "enable-instant-delinquency-calculation"; public static final String PASSWORD_REUSE_CHECK_HISTORY_COUNT = "password-reuse-check-history-count"; public static final String FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT = "allow-force-withdrawal-on-savings-account"; public static final String FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT = "force-withdrawal-on-savings-account-limit"; diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java index bc00b2525ab..5a8d652f228 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java @@ -26,6 +26,7 @@ import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.UUID; @@ -38,13 +39,16 @@ import org.apache.fineract.client.models.DeleteWorkingCapitalLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetConfigurableAttributes; import org.apache.fineract.client.models.GetPaymentAllocation; +import org.apache.fineract.client.models.GetWorkingCapitalLoanDelinquencyRangeScheduleTagHistoryResponse; import org.apache.fineract.client.models.GetWorkingCapitalLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetWorkingCapitalLoanProductsResponse; import org.apache.fineract.client.models.GetWorkingCapitalLoanProductsTemplateResponse; +import org.apache.fineract.client.models.InternalWorkingCapitalLoanPaymentRequest; import org.apache.fineract.client.models.PostAllowAttributeOverrides; import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsRequest; import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsRequest.AccountingRuleEnum; import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsResponse; +import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse; import org.apache.fineract.client.models.PutWorkingCapitalLoanProductsProductIdRequest; import org.apache.fineract.client.models.PutWorkingCapitalLoanProductsProductIdResponse; import org.apache.fineract.client.models.StringEnumOptionData; @@ -59,6 +63,7 @@ import org.apache.fineract.test.stepdef.AbstractStepDef; import org.apache.fineract.test.support.TestContextKey; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Assertions; @Slf4j @RequiredArgsConstructor @@ -719,6 +724,40 @@ private void assertGLAccountMappingId(final Map mappings, final Strin assertions.assertAll(); } + @When("make Internal Payment {string} on {string}") + public void internalPayWCLoan(String amount, String transactionDate) { + PostWorkingCapitalLoansResponse workingCapitalLoanProductsResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + Long resourceId = workingCapitalLoanProductsResponse.getResourceId(); + fineractFeignClient.workingCapitalLoans().payment(resourceId, new InternalWorkingCapitalLoanPaymentRequest() + .amount(BigDecimal.valueOf(Double.parseDouble(amount))).transactionDate(LocalDate.parse(transactionDate))); + } + + @Then("Delinquency Tag History for WC Loan has lines:") + public void checkDelinquencyHistory(final DataTable table) { + PostWorkingCapitalLoansResponse workingCapitalLoanProductsResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + Long resourceId = workingCapitalLoanProductsResponse.getResourceId(); + List actualLines = ok( + () -> fineractFeignClient.workingCapitalLoans().getDelinquencyRangeScheduleTagHistoryById(resourceId)); + log.info("Loan {}", actualLines); + List> rows = table.asLists(); + Assertions.assertEquals(rows.size() - 1, actualLines.size()); + for (int i = 0; i < rows.size() - 1; i++) { + GetWorkingCapitalLoanDelinquencyRangeScheduleTagHistoryResponse actual = actualLines.get(i); + Assertions.assertNotNull(actual); + List expected = rows.get(i + 1); + Assertions.assertEquals(expected.get(0), actual.getPeriodNumber() != null ? actual.getPeriodNumber().toString() : null); + Assertions.assertEquals(expected.get(1), actual.getAddedOnDate() != null ? actual.getAddedOnDate().toString() : null); + Assertions.assertEquals(expected.get(2), actual.getLiftedOnDate() != null ? actual.getLiftedOnDate().toString() : null); + + Assertions.assertNotNull(actual.getDelinquencyRange()); + Assertions.assertEquals(expected.get(3), actual.getDelinquencyRange().getClassification()); + Assertions.assertEquals(expected.get(4), actual.getDelinquencyRange().getMinimumAgeDays() == null ? null + : actual.getDelinquencyRange().getMinimumAgeDays().toString()); + Assertions.assertEquals(expected.get(5), actual.getDelinquencyRange().getMaximumAgeDays() == null ? null + : actual.getDelinquencyRange().getMaximumAgeDays().toString()); + } + } + public PostWorkingCapitalLoanProductsResponse createWorkingCapitalLoanProduct( PostWorkingCapitalLoanProductsRequest workingCapitalProductRequest) { String workingCapitalProductName = workingCapitalProductRequest.getName(); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature index 765e91069ce..ca4c389b0a8 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature @@ -182,3 +182,104 @@ Feature: Working Capital Delinquency #TODO check amounts in case of repayment @Skip @TestRailId:tempX Scenario: Verify working capital loan delinquency range schedule - UCXX: delinquency range schedule with repayments + + + + @TestRailId:CXXXXX1 + Scenario: Working Capital Loan COB + Delinquency UC1 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 0.0 | + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful on "01 January 2026" with "100" EUR transaction amount + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | 0.0 | + When Admin sets the business date to "02 January 2026" + And Admin runs inline COB job for Working Capital Loan + ## Should be ok + Then Delinquency Tag History for WC Loan has lines: + | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + When Admin sets the business date to "30 January 2026" + And Admin runs inline COB job for Working Capital Loan + ## Should be ok + Then Delinquency Tag History for WC Loan has lines: + | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan + ## Should be delinquent for 1 days and Delinquency range 1 + Then Delinquency Tag History for WC Loan has lines: + | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | | D00 | 1 | 30 | + + When Admin sets the business date to "01 April 2026" + And Admin runs inline COB job for Working Capital Loan + Then Delinquency Tag History for WC Loan has lines: + | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-04-01 | | D60 | 61 | 90 | + | 2 | 2026-04-01 | | D30 | 31 | 60 | + | 3 | 2026-04-01 | | D00 | 1 | 30 | + | 1 | 2026-03-02 | | D30 | 31 | 60 | + | 2 | 2026-03-02 | | D00 | 1 | 30 | + | 1 | 2026-01-31 | | D00 | 1 | 30 | + + @TestRailId:CXXXXX2 + Scenario: Working Capital Loan COB + Delinquency UC2 + When Admin sets the business date to "01 December 2020" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 December 2020 | 01 December 2020 | 1800 | 1800 | 1 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2020-12-01 | 2020-12-01 | Submitted and pending approval | 1800.0 | 0.0 | 1800.0 | 1.0 | 0.0 | + Then Admin successfully approves the working capital loan on "01 December 2020" with "1800" amount and expected disbursement date on "01 December 2020" + Then Admin successfully disburse the Working Capital loan on "01 December 2020" with "1800" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful on "01 December 2020" with "1800" EUR transaction amount + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP |2020-12-01 | 2020-12-01 | Active | 1800.0 | 1800.0 | 1800.0 | 1.0 | 0.0 | + When Admin sets the business date to "02 December 2020" + And Admin runs inline COB job for Working Capital Loan + ## Should be ok + When Admin sets the business date to "05 December 2020" + And Admin runs inline COB job for Working Capital Loan + When make Internal Payment "30.0" on "2020-12-05" + + Then Delinquency Tag History for WC Loan has lines: + | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + When Admin sets the business date to "01 January 2021" + And Admin runs inline COB job for Working Capital Loan + ## Should be ok + Then Delinquency Tag History for WC Loan has lines: + | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2020-12-31 | | D00 | 1 | 30 | + + When Admin sets the business date to "06 January 2021" + And Admin runs inline COB job for Working Capital Loan + Then Delinquency Tag History for WC Loan has lines: + | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2020-12-31 | | D00 | 1 | 30 | + When make Internal Payment "54.0" on "2021-01-06" + Then Delinquency Tag History for WC Loan has lines: + | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2020-12-31 | 2021-01-06 | D00 | 1 | 30 | + + When Admin sets the business date to "07 January 2021" + And Admin runs inline COB job for Working Capital Loan + Then Delinquency Tag History for WC Loan has lines: + | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2020-12-31 | 2021-01-06 | D00 | 1 | 30 | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature index b1389e3c79b..a5adce0425c 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature @@ -8,9 +8,10 @@ Feature: Working Capital COB Job Scenario: Verify WC COB job registration, default business step, and scheduler metadata Then Admin checks that configured business jobs contain "WORKING_CAPITAL_LOAN_CLOSE_OF_BUSINESS" Then Admin verifies configured business steps for "WORKING_CAPITAL_LOAN_CLOSE_OF_BUSINESS" match: - | stepName | order | - | DUMMY_BUSINESS_STEP | 1 | - | WC_DELINQUENCY_RANGE_SCHEDULE | 2 | + | stepName | order | + | DUMMY_BUSINESS_STEP | 1 | + | WC_DELINQUENCY_RANGE_SCHEDULE | 2 | + | WC_LOAN_DELINQUENCY_CLASSIFICATION | 3 | Then Admin verifies scheduler job "WC_COB" has display name "Working Capital Loan COB" Then Admin verifies scheduler job "WC_COB" has active status "false" diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/WorkingCapitalLoanDelinquencyClassificationBusinessStep.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/WorkingCapitalLoanDelinquencyClassificationBusinessStep.java new file mode 100644 index 00000000000..e5acfe4144b --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/WorkingCapitalLoanDelinquencyClassificationBusinessStep.java @@ -0,0 +1,86 @@ +/** + * 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.cob.workingcapitalloan.businessstep; + +import static org.apache.fineract.infrastructure.core.diagnostics.performance.MeasuringUtil.measure; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanDelinquencyClassificationService; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class WorkingCapitalLoanDelinquencyClassificationBusinessStep extends WorkingCapitalLoanCOBBusinessStep { + + private final WorkingCapitalLoanDelinquencyClassificationService delinquencyClassificationService; + + @Override + public WorkingCapitalLoan execute(WorkingCapitalLoan loan) { + if (loan == null) { + log.debug("Ignoring Working Capital delinquency tag processing for null loan."); + return null; + } + String externalId = Optional.ofNullable(loan.getExternalId()).map(ExternalId::getValue).orElse(null); + measure(() -> setDelinquencyBucketTags(loan, externalId), duration -> { + log.debug( + "Ending Working Capital delinquency tag processing for loan with Id [{}], account number [{}], external Id [{}], finished in [{}]ms", + loan.getId(), loan.getAccountNumber(), externalId, duration.toMillis()); + }); + return loan; + } + + public void setDelinquencyBucketTags(WorkingCapitalLoan loan, String externalId) { + try { + log.debug( + "Starting Working Capital delinquency tag processing for Working Capital Loan with Id [{}], account number [{}], external Id [{}]", + loan.getId(), loan.getAccountNumber(), externalId); + + if (loan.getLoanProductRelatedDetails() != null && loan.getLoanProductRelatedDetails().getDelinquencyBucket() != null) { + log.debug("Evaluate {} Working Capital Delinquency bucket", loan.getLoanProductRelatedDetails().getDelinquencyBucket()); + delinquencyClassificationService.classifyDelinquency(loan, ThreadLocalContextUtil.getBusinessDate().plusDays(1), + loan.getLoanProductRelatedDetails().getDelinquencyBucket()); + } else { + log.debug("Skipping... Delinquency bucket is not configured for Working Capital Loan {}.", loan.getId()); + } + } catch (RuntimeException re) { + log.error( + "Received exception while processing delinquency tag for Working Capital Loan with Id [{}], account number [{}], external Id [{}]", + loan.getId(), loan.getAccountNumber(), externalId, re); + + throw re; + } + } + + @Override + public String getEnumStyledName() { + return "WC_LOAN_DELINQUENCY_CLASSIFICATION"; + } + + @Override + public String getHumanReadableName() { + return "Working Capital Loan Delinquency Classification Business Step"; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java index 9419291035b..5fd94e16a69 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java @@ -39,11 +39,13 @@ import org.apache.fineract.infrastructure.core.boot.FineractProfiles; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.workingcapitalloan.data.InternalWorkingCapitalLoanPaymentRequest; import org.apache.fineract.portfolio.workingcapitalloan.data.ProjectedAmortizationScheduleGenerateRequest; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDisbursementDetails; import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; +import org.apache.fineract.portfolio.workingcapitalloan.service.InternalWorkingCapitalLoanPaymentService; import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanAmortizationScheduleWriteService; import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanDelinquencyRangeScheduleService; import org.springframework.beans.factory.InitializingBean; @@ -62,6 +64,7 @@ public class InternalWorkingCapitalLoanApiResource implements InitializingBean { private final WorkingCapitalLoanAmortizationScheduleWriteService writeService; private final WorkingCapitalLoanRepository loanRepository; private final WorkingCapitalLoanDelinquencyRangeScheduleService rangeScheduleService; + private final InternalWorkingCapitalLoanPaymentService paymentService; @Override @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") @@ -150,4 +153,21 @@ public Response generateNextDelinquencyPeriod(@PathParam("loanId") @Parameter(de log.info("Generated next delinquency period for WC loan {} with business date {} (TEST ONLY)", loanId, businessDate); return Response.ok().build(); } + + @POST + @Path("{loanId}/internalMakePayment") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Makes Payment (testing)", description = """ + Makes payment for testing purposes. + + DO NOT USE THIS IN PRODUCTION! In the real flow, the schedule will be \ + generated during loan approval/disbursement from the loan and product data.""") + @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "404", description = "Working Capital Loan not found") }) + public void payment(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + final InternalWorkingCapitalLoanPaymentRequest request) { + paymentService.makePayment(loanId, request.getAmount(), request.getTransactionDate()); + } + } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java index 8c3fd2a0e24..47f2362eb2a 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java @@ -20,6 +20,7 @@ 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; @@ -35,7 +36,10 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; +import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; @@ -49,9 +53,11 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyTagHistoryData; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanTemplateData; import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanApplicationReadPlatformService; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanDelinquencyReadPlatformService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; @@ -67,6 +73,7 @@ public class WorkingCapitalLoanApiResource { private final PlatformSecurityContext context; private final WorkingCapitalLoanApplicationReadPlatformService readPlatformService; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final WorkingCapitalLoanDelinquencyReadPlatformService workingCapitalLoanDelinquencyReadPlatformService; @GET @Path("template") @@ -186,6 +193,39 @@ public CommandProcessingResult deleteLoanApplication( return deleteLoanApplication(null, loanExternalId); } + @GET + @Path("{loanId}/delinquencyrangetags") + @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Retrieve the Loan Delinquency Tag history using the Loan Id", description = "", operationId = "getDelinquencyRangeScheduleTagHistoryById") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.GetWorkingCapitalLoanDelinquencyRangeScheduleTagHistoryResponse.class)))) }) + public List getDelinquencyRangeScheduleTagHistoryById( + @PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, @Context final UriInfo uriInfo) { + return getDelinquencyRangeScheduleTagHistory(loanId, null, uriInfo); + } + + @GET + @Path("external-id/{externalId}/delinquencyrangetags") + @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Retrieve the Loan Delinquency Tag history using the Loan Id", description = "", operationId = "getDelinquencyRangeScheduleTagHistoryById") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.GetWorkingCapitalLoanDelinquencyRangeScheduleTagHistoryResponse.class)))) }) + public List getDelinquencyRangeScheduleTagHistoryById( + @PathParam("externalId") @Parameter(description = "externalId", required = true) final String loanExternalId, + @Context final UriInfo uriInfo) { + return getDelinquencyRangeScheduleTagHistory(null, loanExternalId, uriInfo); + } + + private List getDelinquencyRangeScheduleTagHistory(final Long loanId, + final String loanExternalIdStr, final UriInfo uriInfo) { + context.authenticatedUser().validateHasReadPermission("DELINQUENCY_TAGS"); + final Long resolvedLoanId = loanId == null ? readPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr)) + : loanId; + return workingCapitalLoanDelinquencyReadPlatformService.retrieveDelinquencyRangeScheduleTagHistory(resolvedLoanId); + } + @POST @Path("{loanId}") @Consumes({ MediaType.APPLICATION_JSON }) diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java index c1e323dbc16..8b6c88c8d6b 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java @@ -25,6 +25,7 @@ import java.util.List; import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.workingcapitalloanproduct.api.WorkingCapitalLoanProductApiResourceSwagger; @@ -207,6 +208,8 @@ private GetWorkingCapitalLoansLoanIdResponse() {} public GetBalance balance; @Schema(description = "Transaction history (e.g. disbursement).") public List transactions; + @Schema(description = "Working Capital Delinquency Collection Data") + public WorkingCapitalCollection collectionData; } @Schema(description = "Working capital loan running balances") @@ -520,4 +523,82 @@ private PutWorkingCapitalLoansLoanIdDiscountRequest() {} @Schema(example = "dd MMMM yyyy") public String dateFormat; } + + @Schema(description = "Working Capital Delinquency Collection Data") + public static final class WorkingCapitalCollection { + + private WorkingCapitalCollection() {} + + @Schema(description = "Working capital loan delinquency collection summary", example = "true") + public Long delinquentDays; + @Schema(description = "Date when the loan became delinquent", example = "[2024, 1, 15]") + public LocalDate delinquentDate; + @Schema(description = "Total delinquent amount", example = "1234.56") + public BigDecimal delinquentAmount; + @Schema(description = "Pause periods during which delinquency is not counted") + public Collection delinquencyPausePeriods; + @Schema(description = "Delinquency amounts grouped by age range") + public Collection rangeLevelDelinquency; + @Schema(description = "Delinquent principal amount", example = "1000.00") + public BigDecimal delinquentPrincipal; + @Schema(description = "Delinquent fee amount", example = "150.00") + public BigDecimal delinquentFee; + @Schema(description = "Delinquent penalty amount", example = "84.56") + public BigDecimal delinquentPenalty; + + @Schema(description = "Delinquency amount for a specific age range") + public static final class WorkingCapitalCollectionRangeScheduleDelinquency { + + private WorkingCapitalCollectionRangeScheduleDelinquency() {} + + @Schema(description = "Delinquency range id", example = "1") + public Long rangeId; + @Schema(description = "Classification for the delinquency range", example = "Current") + public String classification; + @Schema(description = "Minimum age in days for the range", example = "1") + public Integer minimumAgeDays; + @Schema(description = "Maximum age in days for the range", example = "30") + public Integer maximumAgeDays; + @Schema(description = "Delinquent amount for this range", example = "123.45") + public BigDecimal delinquentAmount; + } + + @Schema(description = "Pause period during which delinquency tracking is paused") + public static final class WorkingCapitalCollectionDelinquencyPausePeriod { + + private WorkingCapitalCollectionDelinquencyPausePeriod() {} + + @Schema(description = "Whether the pause period is active", example = "true") + public boolean active; + @Schema(description = "Pause period start date", example = "[2024, 1, 1]") + public LocalDate pausePeriodStart; + @Schema(description = "Pause period end date", example = "[2024, 1, 31]") + public LocalDate pausePeriodEnd; + } + } + + @Schema(description = "GetWorkingCapitalLoanDelinquencyTagHistoryResponse") + public static final class GetWorkingCapitalLoanDelinquencyRangeScheduleTagHistoryResponse { + + private GetWorkingCapitalLoanDelinquencyRangeScheduleTagHistoryResponse() {} + + @Schema(example = "1") + public Long id; + @Schema(example = "10") + public Long loanId; + public DelinquencyRangeData delinquencyRange; + @Schema(example = "2013,1,2") + public LocalDate addedOnDate; + @Schema(example = "2013,2,20") + public LocalDate liftedOnDate; + @Schema(example = "10") + public Long delinquentDays; + @Schema(example = "1") + public Long rangeId; + @Schema(example = "2") + public Integer periodNumber; + @Schema(example = "123.45") + public BigDecimal delinquentAmount; + } + } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/InternalWorkingCapitalLoanPaymentRequest.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/InternalWorkingCapitalLoanPaymentRequest.java new file mode 100644 index 00000000000..445a7404b06 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/InternalWorkingCapitalLoanPaymentRequest.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 lombok.Data; + +@Data +public class InternalWorkingCapitalLoanPaymentRequest { + + private BigDecimal amount; + private LocalDate transactionDate; +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanCollectionData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanCollectionData.java new file mode 100644 index 00000000000..f116f3d8550 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanCollectionData.java @@ -0,0 +1,49 @@ +/** + * 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 java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.apache.fineract.portfolio.loanaccount.data.DelinquencyPausePeriod; + +@Data +@AllArgsConstructor +public class WorkingCapitalLoanCollectionData { + + private Long delinquentDays; + private LocalDate delinquentDate; + private BigDecimal delinquentAmount; + + public List delinquencyPausePeriods; + public List rangeLevelDelinquency; + + private BigDecimal delinquentPrincipal; + private BigDecimal delinquentFee; + private BigDecimal delinquentPenalty; + + public static WorkingCapitalLoanCollectionData initializeEmptyData() { + return new WorkingCapitalLoanCollectionData(0L, null, BigDecimal.ZERO, null, new ArrayList<>(), BigDecimal.ZERO, BigDecimal.ZERO, + BigDecimal.ZERO); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java index d08735d74d8..da1fc25449c 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java @@ -78,4 +78,6 @@ public class WorkingCapitalLoanData implements Serializable { private List transactions; private Integer delinquencyGraceDays; private StringEnumOptionData delinquencyStartType; + + private WorkingCapitalLoanCollectionData collectionData; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanDelinquencyTagHistoryData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanDelinquencyTagHistoryData.java new file mode 100644 index 00000000000..774f31ccf33 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanDelinquencyTagHistoryData.java @@ -0,0 +1,65 @@ +/** + * 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. + */ + +/** + * 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.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; + +@ToString +@AllArgsConstructor +@Getter +@Setter +public class WorkingCapitalLoanDelinquencyTagHistoryData implements Serializable { + + private Long id; + private Long loanId; + private DelinquencyRangeData delinquencyRange; + private LocalDate addedOnDate; + private LocalDate liftedOnDate; + private Long delinquentDays; + private Long rangeId; + private Integer periodNumber; + private BigDecimal delinquentAmount; +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanRangeScheduleDelinquencyData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanRangeScheduleDelinquencyData.java new file mode 100644 index 00000000000..18ac9fdf817 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanRangeScheduleDelinquencyData.java @@ -0,0 +1,38 @@ +/** + * 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 lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@AllArgsConstructor +@Builder +public class WorkingCapitalLoanRangeScheduleDelinquencyData { + + private Long rangeId; + private String classification; + private Integer minimumAgeDays; + private Integer maximumAgeDays; + private BigDecimal delinquentAmount; + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanDelinquencyRangeScheduleTagHistory.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanDelinquencyRangeScheduleTagHistory.java new file mode 100644 index 00000000000..7fe708a928f --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanDelinquencyRangeScheduleTagHistory.java @@ -0,0 +1,77 @@ +/** + * 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.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +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.DelinquencyRange; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "m_wc_loan_range_delinquency_tag") +public class WorkingCapitalLoanDelinquencyRangeScheduleTagHistory extends AbstractAuditableWithUTCDateTimeCustom { + + @ManyToOne + @JoinColumn(name = "delinquency_range_id", nullable = false) + private DelinquencyRange delinquencyRange; + + @ManyToOne + @JoinColumn(name = "loan_id", nullable = false) + private WorkingCapitalLoan loan; + + @ManyToOne + @JoinColumn(name = "range_id", nullable = false) + private WorkingCapitalLoanDelinquencyRangeSchedule rangeSchedule; + + @Column(name = "addedon_date", nullable = false) + private LocalDate addedOnDate; + + @Column(name = "liftedon_date", nullable = true) + private LocalDate liftedOnDate; + + @Column(name = "outstanding_amount", scale = 6, precision = 19) + private BigDecimal outstandingAmount; + + @Version + private Long version; + + public WorkingCapitalLoanDelinquencyRangeScheduleTagHistory(DelinquencyRange delinquencyRange, WorkingCapitalLoan loan, + WorkingCapitalLoanDelinquencyRangeSchedule rangeSchedule, LocalDate addedOnDate, LocalDate liftedOnDate, + BigDecimal outstandingAmount) { + this.delinquencyRange = delinquencyRange; + this.loan = loan; + this.rangeSchedule = rangeSchedule; + this.addedOnDate = addedOnDate; + this.liftedOnDate = liftedOnDate; + this.outstandingAmount = outstandingAmount; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper.java new file mode 100644 index 00000000000..5f9eb42969b --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper.java @@ -0,0 +1,58 @@ +/** + * 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.mapper; + +import java.util.List; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyTagHistoryData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanRangeScheduleDelinquencyData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeScheduleTagHistory; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +@Mapper(config = MapstructMapperConfig.class, uses = { DelinquencyRangeMapper.class }) +public interface WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper { + + @Mapping(target = "loanId", source = "source.loan.id") + @Mapping(target = "delinquentDays", ignore = true) + @Mapping(target = "rangeId", source = "source.rangeSchedule.id") + @Mapping(target = "periodNumber", source = "source.rangeSchedule.periodNumber") + @Mapping(target = "delinquentAmount", source = "source.rangeSchedule.delinquentAmount") + WorkingCapitalLoanDelinquencyTagHistoryData map(WorkingCapitalLoanDelinquencyRangeScheduleTagHistory source); + + List map(List sources); + + @AfterMapping + default void calculateTotal(WorkingCapitalLoanDelinquencyRangeScheduleTagHistory source, + @MappingTarget WorkingCapitalLoanDelinquencyTagHistoryData target) { + target.setDelinquentDays(source.getRangeSchedule().getDelinquentDays() - source.getDelinquencyRange().getMinimumAgeDays() + 1); + } + + @Mapping(target = "rangeId", source = "delinquencyRange.id") + @Mapping(target = "classification", source = "delinquencyRange.classification") + @Mapping(target = "minimumAgeDays", source = "delinquencyRange.minimumAgeDays") + @Mapping(target = "maximumAgeDays", source = "delinquencyRange.maximumAgeDays") + @Mapping(target = "delinquentAmount", source = "outstandingAmount") + WorkingCapitalLoanRangeScheduleDelinquencyData mapForCollectionData(WorkingCapitalLoanDelinquencyRangeScheduleTagHistory source); + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java index cfbf797617d..09da0d58e29 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java @@ -72,6 +72,7 @@ public interface WorkingCapitalLoanMapper { @Mapping(target = "transactions", source = "transactions") @Mapping(target = "delinquencyGraceDays", source = "loanProductRelatedDetails.delinquencyGraceDays") @Mapping(target = "delinquencyStartType", source = "loanProductRelatedDetails", qualifiedByName = "delinquencyStartTypeData") + @Mapping(target = "collectionData", ignore = true) WorkingCapitalLoanData toData(WorkingCapitalLoan loan); List toDataList(List loans); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java index 08457f38b0b..f6c72d08631 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java @@ -18,19 +18,28 @@ */ package org.apache.fineract.portfolio.workingcapitalloan.repository; +import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import java.util.Optional; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeSchedule; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface WorkingCapitalLoanDelinquencyRangeScheduleRepository extends JpaRepository { List findByLoanIdOrderByPeriodNumberAsc(Long loanId); + @Query("SELECT SUM(s.delinquentAmount) FROM WorkingCapitalLoanDelinquencyRangeSchedule s WHERE s.loan.id = :loanId AND s.delinquentAmount > 0") + BigDecimal getTotalDelinquentAmount(@Param("loanId") Long loanId); + Optional findTopByLoanIdOrderByPeriodNumberDesc(Long loanId); + List findByLoanIdAndToDateIsBeforeAndMinPaymentCriteriaMet(Long loanId, + LocalDate toDateBefore, Boolean minPaymentCriteriaMet); + Optional findByLoanIdAndFromDateLessThanEqualAndToDateGreaterThanEqual(Long loanId, LocalDate date, LocalDate date2); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository.java new file mode 100644 index 00000000000..40d6362cf47 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository.java @@ -0,0 +1,53 @@ +/** + * 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.time.LocalDate; +import java.util.List; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeSchedule; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeScheduleTagHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository + extends JpaRepository, + CrudRepository { + + List findByRangeSchedule( + WorkingCapitalLoanDelinquencyRangeSchedule rangeSchedule); + + @Modifying + void deleteByLoan(WorkingCapitalLoan loan); + + @Modifying + void deleteByRangeSchedule(WorkingCapitalLoanDelinquencyRangeSchedule rangeSchedule); + + List findByLoanAndLiftedOnDateOrderByAddedOnDateAsc(WorkingCapitalLoan loan, + LocalDate liftedOnDate); + + List findByRangeScheduleAndLiftedOnDateOrderByAddedOnDateAsc( + WorkingCapitalLoanDelinquencyRangeSchedule rangeSchedule, LocalDate liftedOnDate); + + List findByLoanIdOrderByAddedOnDateDesc(Long loanId); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentService.java new file mode 100644 index 00000000000..a385accb48f --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentService.java @@ -0,0 +1,27 @@ +/** + * 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.math.BigDecimal; +import java.time.LocalDate; + +public interface InternalWorkingCapitalLoanPaymentService { + + void makePayment(Long loanId, BigDecimal amount, LocalDate transactionDate); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentServiceImpl.java new file mode 100644 index 00000000000..773734567b2 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentServiceImpl.java @@ -0,0 +1,55 @@ +/** + * 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 static org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants.ENABLE_INSTANT_DELINQUENCY_CALCULATION; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class InternalWorkingCapitalLoanPaymentServiceImpl implements InternalWorkingCapitalLoanPaymentService { + + private final WorkingCapitalLoanRepository loanRepository; + private final WorkingCapitalLoanDelinquencyRangeScheduleService delinquencyRangeScheduleService; + private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; + private final WorkingCapitalLoanDelinquencyClassificationService delinquencyClassificationService; + + @Override + public void makePayment(Long loanId, BigDecimal amount, LocalDate transactionDate) { + delinquencyRangeScheduleService.applyRepayment(loanId, transactionDate, amount); + if (globalConfigurationRepository.findOneByNameWithNotFoundDetection(ENABLE_INSTANT_DELINQUENCY_CALCULATION).isEnabled()) { + WorkingCapitalLoan workingCapitalLoan = loanRepository.findById(loanId).orElseThrow(); + if (workingCapitalLoan.getLoanProductRelatedDetails() != null + && workingCapitalLoan.getLoanProductRelatedDetails().getDelinquencyBucket() != null) { + delinquencyClassificationService.classifyDelinquency(workingCapitalLoan, transactionDate, + workingCapitalLoan.getLoanProductRelatedDetails().getDelinquencyBucket()); + } + } + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java index a517ad2c44c..a7cf0bacb8a 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java @@ -30,10 +30,12 @@ import org.apache.fineract.infrastructure.core.api.ApiFacingEnum; import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.portfolio.accountdetails.data.WorkingCapitalLoanAccountSummaryData; import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanCollectionData; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanTemplateData; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; @@ -66,6 +68,7 @@ public class WorkingCapitalLoanApplicationReadPlatformServiceImpl implements Wor private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final WorkingCapitalLoanSummaryMapper workingCapitalLoanSummaryMapper; private final WorkingCapitalBreachReadPlatformService breachReadPlatformService; + private final WorkingCapitalLoanDelinquencyReadPlatformService workingCapitalLoanDelinquencyReadPlatformService; @Override public WorkingCapitalLoanTemplateData retrieveTemplate(final Long productId, final Long clientId) { @@ -145,7 +148,11 @@ public Page retrieveAllPaged(final Pageable pageable, fi public WorkingCapitalLoanData retrieveOne(final Long loanId) { final WorkingCapitalLoan loan = this.repository.findByIdWithFullDetails(loanId) .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); - return this.mapper.toData(loan); + WorkingCapitalLoanData data = this.mapper.toData(loan); + WorkingCapitalLoanCollectionData collectionData = workingCapitalLoanDelinquencyReadPlatformService.getCollectionData(loanId, + ThreadLocalContextUtil.getBusinessDate()); + data.setCollectionData(collectionData); + return data; } @Override @@ -154,7 +161,11 @@ public WorkingCapitalLoanData retrieveOne(final ExternalId externalId) { .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(externalId)); final WorkingCapitalLoan loanWithDetails = this.repository.findByIdWithFullDetails(loan.getId()) .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loan.getId())); - return this.mapper.toData(loanWithDetails); + WorkingCapitalLoanData data = this.mapper.toData(loanWithDetails); + WorkingCapitalLoanCollectionData collectionData = workingCapitalLoanDelinquencyReadPlatformService.getCollectionData(loan.getId(), + ThreadLocalContextUtil.getBusinessDate()); + data.setCollectionData(collectionData); + return data; } @Override diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java new file mode 100644 index 00000000000..4b373c4363b --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java @@ -0,0 +1,29 @@ +/** + * 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.time.LocalDate; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public interface WorkingCapitalLoanDelinquencyClassificationService { + + void classifyDelinquency(WorkingCapitalLoan loanId, LocalDate businessDate, DelinquencyBucket delinquencyBucket); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java new file mode 100644 index 00000000000..5b58ddacba0 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java @@ -0,0 +1,144 @@ +/** + * 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.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeSchedule; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeScheduleTagHistory; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class WorkingCapitalLoanDelinquencyClassificationServiceImpl implements WorkingCapitalLoanDelinquencyClassificationService { + + private final WorkingCapitalLoanDelinquencyRangeScheduleRepository delinquencyRangeScheduleRepository; + private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository delinquencyRangeScheduleTagHistoryRepository; + + /** + * Classifies the delinquency of a loan based on the delinquency bucket and the business date. + * + * @param loan + * the loan for which the delinquency range tag should be applied + * @param businessDate + * the date on which the tagging operation is performed + * @param delinquencyBucket + * the delinquency bucket to search within + */ + @Override + public void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate, DelinquencyBucket delinquencyBucket) { + + List delinquencyRangeScheduleList = delinquencyRangeScheduleRepository + .findByLoanIdOrderByPeriodNumberAsc(loan.getId()); + + for (WorkingCapitalLoanDelinquencyRangeSchedule range : delinquencyRangeScheduleList) { + if (range.getToDate().isBefore(businessDate)) { + long rangeDelinquentDays = range.getOutstandingAmount().compareTo(BigDecimal.ZERO) > 0 + ? DateUtils.getDifferenceInDays(range.getToDate(), businessDate) + : 0L; + boolean isDelinquent = rangeDelinquentDays > 0; + + if (isDelinquent) { + range.setDelinquentAmount(range.getOutstandingAmount()); + range.setDelinquentDays(rangeDelinquentDays); + Optional delinquencyRangeByDays = findDelinquencyRangeByDays(delinquencyBucket, + (int) rangeDelinquentDays); + applyDelinquencyTagForRange(loan, range, delinquencyRangeByDays.orElse(null), businessDate); + } else { + range.setDelinquentAmount(BigDecimal.ZERO); + range.setDelinquentDays(0L); + applyDelinquencyTagForRange(loan, range, null, businessDate); + } + } + } + } + + /** + * Finds the delinquency range for a given delinquency bucket and number of days. + * + * @param delinquencyBucket + * the delinquency bucket to search within + * @param delinquentDays + * the number of days the loan is delinquent + * @return an Optional containing the matching delinquency range, or empty if not found + */ + public Optional findDelinquencyRangeByDays(final DelinquencyBucket delinquencyBucket, final Integer delinquentDays) { + return delinquencyBucket.getRanges().stream().filter(dr -> dr.getMinimumAgeDays() <= delinquentDays) + .filter(dr -> dr.getMaximumAgeDays() == null || dr.getMaximumAgeDays() >= delinquentDays).findAny(); + } + + /** + * Applies a delinquency tag for a specific range to the given loan. This method either adds a new tag for the + * current delinquency range or lifts all existing tags if the current range is null. + * + * @param loan + * the loan for which the delinquency range tag should be applied + * @param range + * the delinquency range schedule associated with the tagging operation + * @param currentRange + * the current delinquency range to be applied; can be null to lift all previous tags + * @param businessDate + * the date on which the tagging operation is performed + */ + public void applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final WorkingCapitalLoanDelinquencyRangeSchedule range, + final DelinquencyRange currentRange, final LocalDate businessDate) { + List updatedList = new ArrayList<>(); + List rangeScheduleTagHistoryList = delinquencyRangeScheduleTagHistoryRepository + .findByRangeScheduleAndLiftedOnDateOrderByAddedOnDateAsc(range, null); + + WorkingCapitalLoanDelinquencyRangeScheduleTagHistory last = rangeScheduleTagHistoryList.isEmpty() ? null + : rangeScheduleTagHistoryList.getLast(); + + // do nothing if currentRange is in rangeScheduleTagHistoryList or last and currentRange are null + if ((last == null && currentRange == null) || (last != null && currentRange != null && rangeScheduleTagHistoryList.stream() + .anyMatch(tag -> Objects.equals(tag.getDelinquencyRange().getId(), currentRange.getId())))) { + return; + } + + if (currentRange == null) { + // lift all previous tags + rangeScheduleTagHistoryList.forEach(tag -> tag.setLiftedOnDate(businessDate)); + updatedList.addAll(rangeScheduleTagHistoryList); + } else { + // add current range + WorkingCapitalLoanDelinquencyRangeScheduleTagHistory newTag = new WorkingCapitalLoanDelinquencyRangeScheduleTagHistory(); + newTag.setLoan(loan); + newTag.setDelinquencyRange(currentRange); + newTag.setRangeSchedule(range); + newTag.setAddedOnDate(businessDate); + newTag.setLiftedOnDate(null); + updatedList.add(newTag); + } + + delinquencyRangeScheduleTagHistoryRepository.saveAll(updatedList); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java index 4909b72bb1b..320c7d492ff 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java @@ -133,15 +133,37 @@ public void generateNextPeriodIfNeeded(WorkingCapitalLoan loan, LocalDate busine @Override public void applyRepayment(Long loanId, LocalDate transactionDate, BigDecimal amount) { - Optional periodOpt = loanDelinquencyRangeScheduleRepository + List pastOpenPeriods = loanDelinquencyRangeScheduleRepository + .findByLoanIdAndToDateIsBeforeAndMinPaymentCriteriaMet(loanId, transactionDate, false); + Optional currentPeriod = loanDelinquencyRangeScheduleRepository .findByLoanIdAndFromDateLessThanEqualAndToDateGreaterThanEqual(loanId, transactionDate, transactionDate); - if (periodOpt.isPresent()) { - WorkingCapitalLoanDelinquencyRangeSchedule period = periodOpt.get(); - BigDecimal newPaidAmount = period.getPaidAmount().add(amount); + BigDecimal transactionAmount = amount; + for (WorkingCapitalLoanDelinquencyRangeSchedule period : pastOpenPeriods) { + BigDecimal payAmount = MathUtil.min(amount, period.getOutstandingAmount(), true); + transactionAmount = transactionAmount.subtract(payAmount); + period.setPaidAmount(period.getPaidAmount().add(payAmount)); + period.setOutstandingAmount(period.getOutstandingAmount().subtract(payAmount)); + if (period.getOutstandingAmount().compareTo(BigDecimal.ZERO) <= 0) { + period.setMinPaymentCriteriaMet(true); + period.setDelinquentAmount(BigDecimal.ZERO); + period.setDelinquentDays(0L); + } + loanDelinquencyRangeScheduleRepository.saveAndFlush(period); + log.debug("Applied repayment of {} to delinquency range schedule period {} for WC loan {}", payAmount, period.getPeriodNumber(), + loanId); + if (transactionAmount.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + } + if (currentPeriod.isPresent()) { + WorkingCapitalLoanDelinquencyRangeSchedule period = currentPeriod.get(); + BigDecimal newPaidAmount = period.getPaidAmount().add(transactionAmount); period.setPaidAmount(newPaidAmount); period.setOutstandingAmount(period.getExpectedAmount().subtract(newPaidAmount).max(BigDecimal.ZERO)); if (newPaidAmount.compareTo(period.getExpectedAmount()) >= 0) { period.setMinPaymentCriteriaMet(true); + period.setDelinquentAmount(BigDecimal.ZERO); + period.setDelinquentDays(0L); } loanDelinquencyRangeScheduleRepository.saveAndFlush(period); log.debug("Applied repayment of {} to delinquency range schedule period {} for WC loan {}", amount, period.getPeriodNumber(), diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformService.java new file mode 100644 index 00000000000..f69d7f9c761 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformService.java @@ -0,0 +1,32 @@ +/** + * 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.time.LocalDate; +import java.util.List; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanCollectionData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyTagHistoryData; + +public interface WorkingCapitalLoanDelinquencyReadPlatformService { + + WorkingCapitalLoanCollectionData getCollectionData(Long loanId, LocalDate businessDate); + + List retrieveDelinquencyRangeScheduleTagHistory(Long loanId); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java new file mode 100644 index 00000000000..36c956442d1 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java @@ -0,0 +1,82 @@ +/** + * 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.math.BigDecimal; +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanCollectionData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyTagHistoryData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanRangeScheduleDelinquencyData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeScheduleTagHistory; +import org.apache.fineract.portfolio.workingcapitalloan.mapper.WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class WorkingCapitalLoanDelinquencyReadPlatformServiceImpl implements WorkingCapitalLoanDelinquencyReadPlatformService { + + private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper delinquencyRangeScheduleTagHistoryMapper; + private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository delinquencyRangeScheduleTagHistoryRepository; + private final WorkingCapitalLoanDelinquencyRangeScheduleRepository delinquencyRangeScheduleRepository; + + @Override + public WorkingCapitalLoanCollectionData getCollectionData(Long loanId, LocalDate businessDate) { + final WorkingCapitalLoanCollectionData template = WorkingCapitalLoanCollectionData.initializeEmptyData(); + List byLoanIdOrderByAddedOnDateDesc = delinquencyRangeScheduleTagHistoryRepository + .findByLoanIdOrderByAddedOnDateDesc(loanId); + List list = byLoanIdOrderByAddedOnDateDesc.stream() + // get active delinquency tags + .filter(x -> x.getLiftedOnDate() == null).map(delinquencyRangeScheduleTagHistoryMapper::mapForCollectionData).toList(); + + Optional oldestDelinquentTag = byLoanIdOrderByAddedOnDateDesc.stream() + .filter(x -> x.getLiftedOnDate() == null) + .min(Comparator.comparing(WorkingCapitalLoanDelinquencyRangeScheduleTagHistory::getAddedOnDate)); + + if (oldestDelinquentTag.isPresent()) { + template.setDelinquentDays(DateUtils.getDifferenceInDays(oldestDelinquentTag.get().getAddedOnDate(), businessDate) + 1); + template.setDelinquentDate(oldestDelinquentTag.get().getAddedOnDate()); + BigDecimal delinquentAmount = delinquencyRangeScheduleRepository.getTotalDelinquentAmount(loanId); + template.setDelinquentAmount(delinquentAmount); + template.setDelinquentPrincipal(delinquentAmount); + } + + template.setRangeLevelDelinquency(list); + + return template; + } + + @Override + public List retrieveDelinquencyRangeScheduleTagHistory(Long loanId) { + List byLoanIdOrderByAddedOnDateDesc = delinquencyRangeScheduleTagHistoryRepository + .findByLoanIdOrderByAddedOnDateDesc(loanId); + return delinquencyRangeScheduleTagHistoryMapper.map(byLoanIdOrderByAddedOnDateDesc); + } +} 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 8ac5eea47c1..598770b624e 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 @@ -45,4 +45,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0024_wc_loan_delinquincy_service.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0024_wc_loan_delinquincy_service.xml new file mode 100644 index 00000000000..73840cad137 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0024_wc_loan_delinquincy_service.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 a3af08ba8b7..54d3c92c806 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 @@ -158,6 +158,7 @@ org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyAction org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeSchedule + org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeScheduleTagHistory org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBalance org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDisbursementDetails diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/GlobalConfigurationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/GlobalConfigurationTest.java index c0ddce87c9e..7facba5f32b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/GlobalConfigurationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/GlobalConfigurationTest.java @@ -23,11 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.builder.ResponseSpecBuilder; -import io.restassured.http.ContentType; -import io.restassured.specification.RequestSpecification; -import io.restassured.specification.ResponseSpecification; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.client.models.GetGlobalConfigurationsResponse; import org.apache.fineract.client.models.GlobalConfigurationPropertyData; @@ -35,7 +30,6 @@ import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; -import org.apache.fineract.integrationtests.common.Utils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -45,16 +39,10 @@ public class GlobalConfigurationTest { - private ResponseSpecification responseSpec; - private RequestSpecification requestSpec; private GlobalConfigurationHelper globalConfigurationHelper; @BeforeEach public void setup() { - Utils.initializeRESTAssured(); - this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); - this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); - this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); globalConfigurationHelper = new GlobalConfigurationHelper(); } @@ -156,4 +144,35 @@ public void testOriginatorCreationConfigurationCanBeEnabled() { new PutGlobalConfigurationsRequest().enabled(config.getEnabled())); } } + + @Test + public void testInstantDelinquencyCalculationConfigurationExists() { + String configName = GlobalConfigurationConstants.ENABLE_INSTANT_DELINQUENCY_CALCULATION; + GlobalConfigurationPropertyData config = globalConfigurationHelper.getGlobalConfigurationByName(configName); + + Assertions.assertNotNull(config, "Configuration should exist"); + assertEquals(configName, config.getName(), "Configuration name should match"); + assertEquals(true, config.getEnabled(), "Configuration should be enabled by default"); + assertEquals(false, config.getTrapDoor(), "Configuration should not be a trap door"); + } + + @Test + public void testInstantDelinquencyCalculationConfigurationCanBeEnabled() { + String configName = GlobalConfigurationConstants.ENABLE_INSTANT_DELINQUENCY_CALCULATION; + GlobalConfigurationPropertyData config = globalConfigurationHelper.getGlobalConfigurationByName(configName); + Assertions.assertNotNull(config); + + try { + globalConfigurationHelper.updateGlobalConfiguration(configName, new PutGlobalConfigurationsRequest().enabled(true)); + GlobalConfigurationPropertyData enabledConfig = globalConfigurationHelper.getGlobalConfigurationByName(configName); + assertEquals(true, enabledConfig.getEnabled(), "Configuration should be enabled after update"); + + globalConfigurationHelper.updateGlobalConfiguration(configName, new PutGlobalConfigurationsRequest().enabled(false)); + GlobalConfigurationPropertyData disabledConfig = globalConfigurationHelper.getGlobalConfigurationByName(configName); + assertEquals(false, disabledConfig.getEnabled(), "Configuration should be disabled after update"); + } finally { + globalConfigurationHelper.updateGlobalConfiguration(configName, + new PutGlobalConfigurationsRequest().enabled(config.getEnabled())); + } + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java index a2fb6a5810e..6152771e009 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java @@ -635,6 +635,13 @@ private static ArrayList getAllDefaultGlobalConfigurations() { allowCashAndNonCashAccrual.put("trapDoor", false); defaults.add(allowCashAndNonCashAccrual); + HashMap enableInstantDelinquencyCalculation = new HashMap<>(); + enableInstantDelinquencyCalculation.put("name", GlobalConfigurationConstants.ENABLE_INSTANT_DELINQUENCY_CALCULATION); + enableInstantDelinquencyCalculation.put("value", 0L); + enableInstantDelinquencyCalculation.put("enabled", true); + enableInstantDelinquencyCalculation.put("trapDoor", false); + defaults.add(enableInstantDelinquencyCalculation); + return defaults; } From 6508f88b5973f0399272069a997617e0d05ccb9d Mon Sep 17 00:00:00 2001 From: Peter Kovacs Date: Thu, 9 Apr 2026 15:14:15 +0200 Subject: [PATCH 2/2] FINERACT-2455: WC - Delinquency Management - Delinquency days & Delinquency Bucket handling - E2E tests --- .../WorkingCapitalLoanAccountStepDef.java | 6 +- .../stepdef/loan/WorkingCapitalStepDef.java | 27 +- .../WcpCobBusinessStepInitializerStep.java | 2 +- .../WorkingCapitalDelinquency.feature | 620 ++++++++++++++++-- .../WorkingCapitalDelinquencyPause.feature | 4 +- ...orkingCapitalDelinquencyReschedule.feature | 6 +- 6 files changed, 579 insertions(+), 86 deletions(-) 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 67ed65c8b3a..5ebd99a242e 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 @@ -869,10 +869,10 @@ private void createWorkingCapitalLoanAccount(final List loanData) { @SuppressWarnings("unchecked") private void trackLoanIdIfEnabled(final Long loanId) { - final List trackedIds = testContext().get(TestContextKey.WC_LOAN_IDS); - if (trackedIds != null) { - trackedIds.add(loanId); + if (testContext().get(TestContextKey.WC_LOAN_IDS) == null) { + testContext().set(TestContextKey.WC_LOAN_IDS, new ArrayList<>()); } + ((List) testContext().get(TestContextKey.WC_LOAN_IDS)).add(loanId); } private void modifyWorkingCapitalLoanAccount(final List loanData) { diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java index 5a8d652f228..9b34ed2f8a5 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java @@ -724,21 +724,34 @@ private void assertGLAccountMappingId(final Map mappings, final Strin assertions.assertAll(); } - @When("make Internal Payment {string} on {string}") + private Long getWorkingCapitalLoanResourceId() { + PostWorkingCapitalLoansResponse response = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + return response.getResourceId(); + } + + @When("Admin makes Internal Payment {string} on {string}") public void internalPayWCLoan(String amount, String transactionDate) { - PostWorkingCapitalLoansResponse workingCapitalLoanProductsResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - Long resourceId = workingCapitalLoanProductsResponse.getResourceId(); + Long resourceId = getWorkingCapitalLoanResourceId(); fineractFeignClient.workingCapitalLoans().payment(resourceId, new InternalWorkingCapitalLoanPaymentRequest() .amount(BigDecimal.valueOf(Double.parseDouble(amount))).transactionDate(LocalDate.parse(transactionDate))); } - @Then("Delinquency Tag History for WC Loan has lines:") + @Then("Delinquency Tag History for Working Capital loan has lines:") public void checkDelinquencyHistory(final DataTable table) { - PostWorkingCapitalLoansResponse workingCapitalLoanProductsResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - Long resourceId = workingCapitalLoanProductsResponse.getResourceId(); + Long resourceId = getWorkingCapitalLoanResourceId(); List actualLines = ok( () -> fineractFeignClient.workingCapitalLoans().getDelinquencyRangeScheduleTagHistoryById(resourceId)); - log.info("Loan {}", actualLines); + + // Sort by addedOnDate (descending), then by periodNumber (descending) + actualLines.sort((a, b) -> { + int dateCompare = b.getAddedOnDate().compareTo(a.getAddedOnDate()); + if (dateCompare != 0) { + return dateCompare; + } + return b.getPeriodNumber().compareTo(a.getPeriodNumber()); + }); + + log.debug("Sorted Loan Delinquency History: {}", actualLines); List> rows = table.asLists(); Assertions.assertEquals(rows.size() - 1, actualLines.size()); for (int i = 0; i < rows.size() - 1; i++) { diff --git a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WcpCobBusinessStepInitializerStep.java b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WcpCobBusinessStepInitializerStep.java index 3a4433f9eca..2293659b7dd 100644 --- a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WcpCobBusinessStepInitializerStep.java +++ b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WcpCobBusinessStepInitializerStep.java @@ -41,7 +41,7 @@ public class WcpCobBusinessStepInitializerStep implements FineractGlobalInitiali public void initialize() throws Exception { try { JobBusinessStepConfigData response = workFlowJobHelper.getConfiguredWorkflowSteps(WCP_COB_JOB_NAME); - log.info("WCP COB configured business steps: {}", response.getBusinessSteps()); + log.debug("WCP COB configured business steps: {}", response.getBusinessSteps()); } catch (CallFailedRuntimeException e) { log.warn("WCP COB business steps retrieval failed (expected if WCP COB not deployed): {}", e.getMessage()); log.debug("Full stack trace:", e); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature index ca4c389b0a8..1d5adb95961 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquency.feature @@ -108,7 +108,7 @@ Feature: Working Capital Delinquency And Admin runs inline COB job for Working Capital Loan by loanId Then Working Capital loan delinquency range schedule has the following data: | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | - | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | null | null | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 1 | | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | @TestRailId:C74467 @@ -137,12 +137,12 @@ Feature: Working Capital Delinquency And Admin runs inline COB job for Working Capital Loan by loanId Then Working Capital loan delinquency range schedule has the following data: | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | - | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | null | null | - | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | false | null | null | - | 3 | 2026-03-02 | 2026-03-31 | 270.0 | 0.0 | 270.0 | false | null | null | - | 4 | 2026-04-01 | 2026-04-30 | 270.0 | 0.0 | 270.0 | false | null | null | - | 5 | 2026-05-01 | 2026-05-30 | 270.0 | 0.0 | 270.0 | false | null | null | - | 6 | 2026-05-31 | 2026-06-29 | 270.0 | 0.0 | 270.0 | false | null | null | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 151 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | false | 270.0 | 121 | + | 3 | 2026-03-02 | 2026-03-31 | 270.0 | 0.0 | 270.0 | false | 270.0 | 91 | + | 4 | 2026-04-01 | 2026-04-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 61 | + | 5 | 2026-05-01 | 2026-05-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 31 | + | 6 | 2026-05-31 | 2026-06-29 | 270.0 | 0.0 | 270.0 | false | 270.0 | 1 | | 7 | 2026-06-30 | 2026-07-29 | 270.0 | 0.0 | 270.0 | null | null | null | @TestRailId:C74468 @@ -171,22 +171,16 @@ Feature: Working Capital Delinquency And Admin runs inline COB job for Working Capital Loan by loanId Then Working Capital loan delinquency range schedule has the following data: | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | - | 1 | 2026-01-01 | 2026-01-30 | 300.0 | 0.0 | 300.0 | false | null | null | - | 2 | 2026-01-31 | 2026-03-01 | 300.0 | 0.0 | 300.0 | false | null | null | - | 3 | 2026-03-02 | 2026-03-31 | 300.0 | 0.0 | 300.0 | false | null | null | - | 4 | 2026-04-01 | 2026-04-30 | 300.0 | 0.0 | 300.0 | false | null | null | - | 5 | 2026-05-01 | 2026-05-30 | 300.0 | 0.0 | 300.0 | false | null | null | - | 6 | 2026-05-31 | 2026-06-29 | 300.0 | 0.0 | 300.0 | false | null | null | + | 1 | 2026-01-01 | 2026-01-30 | 300.0 | 0.0 | 300.0 | false | 300.0 | 151 | + | 2 | 2026-01-31 | 2026-03-01 | 300.0 | 0.0 | 300.0 | false | 300.0 | 121 | + | 3 | 2026-03-02 | 2026-03-31 | 300.0 | 0.0 | 300.0 | false | 300.0 | 91 | + | 4 | 2026-04-01 | 2026-04-30 | 300.0 | 0.0 | 300.0 | false | 300.0 | 61 | + | 5 | 2026-05-01 | 2026-05-30 | 300.0 | 0.0 | 300.0 | false | 300.0 | 31 | + | 6 | 2026-05-31 | 2026-06-29 | 300.0 | 0.0 | 300.0 | false | 300.0 | 1 | | 7 | 2026-06-30 | 2026-07-29 | 300.0 | 0.0 | 300.0 | null | null | null | -#TODO check amounts in case of repayment - @Skip @TestRailId:tempX - Scenario: Verify working capital loan delinquency range schedule - UCXX: delinquency range schedule with repayments - - - - @TestRailId:CXXXXX1 - Scenario: Working Capital Loan COB + Delinquency UC1 + @TestRailId:C74525 + Scenario: Verify working capital loan delinquency tag history - UC1: multiple ranges When Admin sets the business date to "01 January 2026" And Admin creates a client with random data And Admin creates a working capital loan with the following data: @@ -203,83 +197,569 @@ Feature: Working Capital Delinquency And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | 0.0 | +# --- No delinquency tag history --- When Admin sets the business date to "02 January 2026" And Admin runs inline COB job for Working Capital Loan - ## Should be ok - Then Delinquency Tag History for WC Loan has lines: - | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | - + Then Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- No delinquency tag history --- When Admin sets the business date to "30 January 2026" And Admin runs inline COB job for Working Capital Loan - ## Should be ok - Then Delinquency Tag History for WC Loan has lines: - | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | - + Then Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Delinquency tag history with 1 range --- When Admin sets the business date to "31 January 2026" And Admin runs inline COB job for Working Capital Loan - ## Should be delinquent for 1 days and Delinquency range 1 - Then Delinquency Tag History for WC Loan has lines: - | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | - | 1 | 2026-01-31 | | D00 | 1 | 30 | - + Then Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | | D00 | 1 | 30 | +# --- Delinquency tag history with 3 ranges--- When Admin sets the business date to "01 April 2026" And Admin runs inline COB job for Working Capital Loan - Then Delinquency Tag History for WC Loan has lines: - | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | - | 1 | 2026-04-01 | | D60 | 61 | 90 | - | 2 | 2026-04-01 | | D30 | 31 | 60 | - | 3 | 2026-04-01 | | D00 | 1 | 30 | - | 1 | 2026-03-02 | | D30 | 31 | 60 | - | 2 | 2026-03-02 | | D00 | 1 | 30 | - | 1 | 2026-01-31 | | D00 | 1 | 30 | + Then Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 3 | 2026-04-01 | | D00 | 1 | 30 | + | 2 | 2026-04-01 | | D30 | 31 | 60 | + | 1 | 2026-04-01 | | D60 | 61 | 90 | + | 2 | 2026-03-02 | | D00 | 1 | 30 | + | 1 | 2026-03-02 | | D30 | 31 | 60 | + | 1 | 2026-01-31 | | D00 | 1 | 30 | - @TestRailId:CXXXXX2 - Scenario: Working Capital Loan COB + Delinquency UC2 + @TestRailId:C74526 + Scenario: Verify working capital loan delinquency tag history - UC2: multiple ranges with (internal) payment When Admin sets the business date to "01 December 2020" And Admin creates a client with random data And Admin creates a working capital loan with the following data: - | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | - | WCLP | 01 December 2020 | 01 December 2020 | 1800 | 1800 | 1 | 0 | + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 December 2020 | 01 December 2020 | 1800 | 1800 | 1 | 0 | Then Working capital loan creation was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | - | WCLP | 2020-12-01 | 2020-12-01 | Submitted and pending approval | 1800.0 | 0.0 | 1800.0 | 1.0 | 0.0 | + | WCLP | 2020-12-01 | 2020-12-01 | Submitted and pending approval | 1800.0 | 0.0 | 1800.0 | 1.0 | 0.0 | Then Admin successfully approves the working capital loan on "01 December 2020" with "1800" amount and expected disbursement date on "01 December 2020" Then Admin successfully disburse the Working Capital loan on "01 December 2020" with "1800" EUR transaction amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful on "01 December 2020" with "1800" EUR transaction amount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | - | WCLP |2020-12-01 | 2020-12-01 | Active | 1800.0 | 1800.0 | 1800.0 | 1.0 | 0.0 | + | WCLP | 2020-12-01 | 2020-12-01 | Active | 1800.0 | 1800.0 | 1800.0 | 1.0 | 0.0 | When Admin sets the business date to "02 December 2020" And Admin runs inline COB job for Working Capital Loan - ## Should be ok +# --- No delinquency tag history --- When Admin sets the business date to "05 December 2020" And Admin runs inline COB job for Working Capital Loan - When make Internal Payment "30.0" on "2020-12-05" - - Then Delinquency Tag History for WC Loan has lines: - | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | - + When Admin makes Internal Payment "30.0" on "2020-12-05" + Then Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Delinquency tag history with 1 range --- When Admin sets the business date to "01 January 2021" And Admin runs inline COB job for Working Capital Loan - ## Should be ok - Then Delinquency Tag History for WC Loan has lines: - | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | - | 1 | 2020-12-31 | | D00 | 1 | 30 | - + Then Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2020-12-31 | | D00 | 1 | 30 | +# --- Delinquency tag history with 1 range + internal payment--- When Admin sets the business date to "06 January 2021" And Admin runs inline COB job for Working Capital Loan - Then Delinquency Tag History for WC Loan has lines: - | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | - | 1 | 2020-12-31 | | D00 | 1 | 30 | - When make Internal Payment "54.0" on "2021-01-06" - Then Delinquency Tag History for WC Loan has lines: - | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | - | 1 | 2020-12-31 | 2021-01-06 | D00 | 1 | 30 | - + Then Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2020-12-31 | | D00 | 1 | 30 | + When Admin makes Internal Payment "54.0" on "2021-01-06" + Then Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2020-12-31 | 2021-01-06 | D00 | 1 | 30 | When Admin sets the business date to "07 January 2021" And Admin runs inline COB job for Working Capital Loan - Then Delinquency Tag History for WC Loan has lines: - | rangeId | getAddedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | - | 1 | 2020-12-31 | 2021-01-06 | D00 | 1 | 30 | + Then Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2020-12-31 | 2021-01-06 | D00 | 1 | 30 | + + @TestRailId:C7457 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC1: full expectedAmount repaid on disbursement day + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + When Admin makes Internal Payment "270.0" on "2026-01-01" + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + Then Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + @TestRailId:C74528 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC2: full expectedAmount repaid after disbursement day + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Full expectedAmount paid --- + When Admin sets the business date to "02 January 2026" + And Admin makes Internal Payment "270.0" on "2026-01-02" + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + @TestRailId:C74529 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC3: full expectedAmount repaid on last day of 1st period + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Full expectedAmount paid --- + When Admin sets the business date to "30 January 2026" + And Admin makes Internal Payment "270.0" on "2026-01-30" + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + @TestRailId:C74530 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC4: full expectedAmount repaid on first day of 2nd period + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Full expectedAmount paid --- + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 1 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | | D00 | 1 | 30 | + And Admin makes Internal Payment "270.0" on "2026-01-31" +# --- Check --- + When Admin sets the business date to "01 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | 2026-01-31 | D00 | 1 | 30 | + + @TestRailId:C74531 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC5: full expectedAmount repaid in 1st period with multiple payments on same day + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Full expectedAmount paid in 2 payments on the same day--- + When Admin sets the business date to "02 January 2026" + And Admin makes Internal Payment "170.0" on "2026-01-02" + And Admin makes Internal Payment "100.0" on "2026-01-02" + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + @TestRailId:C74532 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC6: full expectedAmount repaid in 1st period with multiple payments on different days + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Full expectedAmount paid in 2 payments on different days--- + When Admin sets the business date to "02 January 2026" + And Admin makes Internal Payment "170.0" on "2026-01-02" + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 170.0 | 100.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "15 January 2026" + And Admin makes Internal Payment "100.0" on "2026-01-15" + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + @TestRailId:C74533 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC7: partial expectedAmount repaid in 1st period + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Partial expectedAmount paid --- + When Admin sets the business date to "02 January 2026" + And Admin makes Internal Payment "170.0" on "2026-01-02" + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 170.0 | 100.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 170.0 | 100.0 | false | 100.0 | 1 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | | D00 | 1 | 30 | + + @TestRailId:C74534 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC8: partial expectedAmount repaid in 2nd period + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Start of 2nd period --- + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 1 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | | D00 | 1 | 30 | + # --- Partial expectedAmount paid --- + When Admin sets the business date to "10 February 2026" + And Admin makes Internal Payment "170.0" on "2026-02-10" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 170.0 | 100.0 | false | 100.0 | 11 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | | D00 | 1 | 30 | + + @TestRailId:C74535 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC9: expectedAmount overpaid in 1st period + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + # --- expectedAmount overpaid --- + When Admin sets the business date to "10 January 2026" + And Admin makes Internal Payment "370.0" on "2026-01-10" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 370.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + # --- Start of 2nd period --- + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 370.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + @TestRailId:C74536 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC10: expectedAmount overpaid in 2nd period + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Start of 2nd period --- + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 1 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | | D00 | 1 | 30 | + # --- expectedAmount overpaid --- + When Admin sets the business date to "10 February 2026" + And Admin makes Internal Payment "370.0" on "2026-02-10" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 100.0 | 170.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | 2026-02-10 | D00 | 1 | 30 | + + @TestRailId:C74537 + Scenario: Verify working capital loan delinquency range schedule with (internal) payments - UC11: expectedAmount overpaid in late period + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 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" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | 0.0 | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Late period --- + When Admin sets the business date to "01 May 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 91 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | false | 270.0 | 61 | + | 3 | 2026-03-02 | 2026-03-31 | 270.0 | 0.0 | 270.0 | false | 270.0 | 31 | + | 4 | 2026-04-01 | 2026-04-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 1 | + | 5 | 2026-05-01 | 2026-05-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 4 | 2026-05-01 | | D00 | 1 | 30 | + | 3 | 2026-05-01 | | D30 | 31 | 60 | + | 2 | 2026-05-01 | | D60 | 61 | 90 | + | 1 | 2026-05-01 | | D90 | 91 | 120 | + | 3 | 2026-04-01 | | D00 | 1 | 30 | + | 2 | 2026-04-01 | | D30 | 31 | 60 | + | 1 | 2026-04-01 | | D60 | 61 | 90 | + | 2 | 2026-03-02 | | D00 | 1 | 30 | + | 1 | 2026-03-02 | | D30 | 31 | 60 | + | 1 | 2026-01-31 | | D00 | 1 | 30 | + # --- expectedAmount overpaid --- + When Admin sets the business date to "10 May 2026" + And Admin makes Internal Payment "1500.0" on "2026-05-10" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 3 | 2026-03-02 | 2026-03-31 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 4 | 2026-04-01 | 2026-04-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 5 | 2026-05-01 | 2026-05-30 | 270.0 | 420.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 4 | 2026-05-01 | 2026-05-10 | D00 | 1 | 30 | + | 3 | 2026-05-01 | 2026-05-10 | D30 | 31 | 60 | + | 2 | 2026-05-01 | 2026-05-10 | D60 | 61 | 90 | + | 1 | 2026-05-01 | 2026-05-10 | D90 | 91 | 120 | + | 3 | 2026-04-01 | 2026-05-10 | D00 | 1 | 30 | + | 2 | 2026-04-01 | 2026-05-10 | D30 | 31 | 60 | + | 1 | 2026-04-01 | 2026-05-10 | D60 | 61 | 90 | + | 2 | 2026-03-02 | 2026-05-10 | D00 | 1 | 30 | + | 1 | 2026-03-02 | 2026-05-10 | D30 | 31 | 60 | + | 1 | 2026-01-31 | 2026-05-10 | D00 | 1 | 30 | \ No newline at end of file diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyPause.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyPause.feature index b664047ac55..de997a39bf8 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyPause.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyPause.feature @@ -154,7 +154,7 @@ Feature: Working Capital Delinquency Pause | PAUSE | 2026-02-15 | 2026-02-25 | And Working Capital loan delinquency range schedule has the following data: | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | - | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | null | null | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 16 | | 2 | 2026-01-31 | 2026-03-11 | 270.0 | 0.0 | 270.0 | null | null | null | @TestRailId:C74485 @@ -197,7 +197,7 @@ Feature: Working Capital Delinquency Pause And Admin runs inline COB job for Working Capital Loan by loanId And Working Capital loan delinquency range schedule has the following data: | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | - | 1 | 2026-01-01 | 2026-03-12 | 270.0 | 0.0 | 270.0 | false | null | null | + | 1 | 2026-01-01 | 2026-03-12 | 270.0 | 0.0 | 270.0 | false | 270.0 | 3 | | 2 | 2026-03-13 | 2026-04-11 | 270.0 | 0.0 | 270.0 | null | null | null | @TestRailId:C74486 diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReschedule.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReschedule.feature index 0f01fa38050..c098e0a88ab 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReschedule.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyReschedule.feature @@ -240,9 +240,9 @@ Feature: Working Capital Delinquency Reschedule Action When Admin runs inline COB job for Working Capital Loan Then WC loan delinquency range schedule periods have specific data: | periodNumber | expectedAmount | outstandingAmount | delinquentDays | delinquentAmount | - | 1 | 300 | 300 | | | - | 5 | 300 | 300 | | | - | 6 | 100 | 100 | | | + | 1 | 300 | 300 | 197 | 300 | + | 5 | 300 | 300 | 77 | 300 | + | 6 | 100 | 100 | 47 | 100 | | 8 | 100 | 100 | | | @TestRailId:C74503