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/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 bc00b2525ab..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 @@ -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,53 @@ private void assertGLAccountMappingId(final Map mappings, final Strin assertions.assertAll(); } + 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) { + Long resourceId = getWorkingCapitalLoanResourceId(); + fineractFeignClient.workingCapitalLoans().payment(resourceId, new InternalWorkingCapitalLoanPaymentRequest() + .amount(BigDecimal.valueOf(Double.parseDouble(amount))).transactionDate(LocalDate.parse(transactionDate))); + } + + @Then("Delinquency Tag History for Working Capital loan has lines:") + public void checkDelinquencyHistory(final DataTable table) { + Long resourceId = getWorkingCapitalLoanResourceId(); + List actualLines = ok( + () -> fineractFeignClient.workingCapitalLoans().getDelinquencyRangeScheduleTagHistoryById(resourceId)); + + // 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++) { + 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/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 765e91069ce..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,14 +171,595 @@ 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: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: + | 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 | +# --- No delinquency tag history --- + When Admin sets the business date to "02 January 2026" + And Admin runs inline COB job for Working Capital Loan + 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 + 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 + 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 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: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 | + 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 +# --- 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 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 + 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 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 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 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; }