diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalTenantDateHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalTenantDateHelper.java
new file mode 100644
index 00000000000..720c4b5b4f5
--- /dev/null
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalTenantDateHelper.java
@@ -0,0 +1,81 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.test.helper;
+
+import static org.apache.fineract.client.feign.util.FeignCalls.ok;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.LocalDate;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.test.support.TestContext;
+import org.apache.fineract.test.support.TestContextKey;
+import org.springframework.stereotype.Component;
+
+/**
+ * Resolves the date a Working Capital loan action is stamped with while {@code enable-business-date} is switched off.
+ * The business date API cannot be used for that: it returns the persisted business-date row even when the configuration
+ * is disabled. The read-only charge-off template exposes a date resolved through
+ * {@code DateUtils.getBusinessLocalDate}, the same resolver the stamps use, and therefore returns the tenant date while
+ * the configuration is disabled.
+ */
+@Component
+@RequiredArgsConstructor
+public class WorkingCapitalTenantDateHelper {
+
+ private static final String CHARGE_OFF_TEMPLATE = "chargeOff";
+
+ private final FineractFeignClient fineractClient;
+ private final BusinessDateHelper businessDateHelper;
+
+ /**
+ * Captures the server's effective date immediately before an action that should be stamped with the tenant date.
+ */
+ public void captureCurrentTenantDateBeforeAction(final Long loanId) {
+ TestContext.INSTANCE.set(TestContextKey.WORKING_CAPITAL_CURRENT_TENANT_DATE_BEFORE_ACTION, getEffectiveDateFromServer(loanId));
+ }
+
+ /**
+ * Accepts the server date captured immediately before or after the action. Usually they are identical; accepting
+ * both makes the assertion deterministic when the action crosses midnight in the tenant timezone.
+ *
+ *
+ * The probe and the stamp share one resolver, so on their own they cannot tell a correct fallback from a regression
+ * that returns the stored business-date row for both. The stored row is therefore required to differ from the
+ * tenant date: the scenarios park it on a date in the past for exactly this reason.
+ */
+ public void assertStampedOnCurrentTenantDate(final LocalDate actual, final Long loanId, final String description) {
+ final LocalDate tenantDateBeforeAction = TestContext.INSTANCE.get(TestContextKey.WORKING_CAPITAL_CURRENT_TENANT_DATE_BEFORE_ACTION);
+ final LocalDate tenantDateAfterAction = getEffectiveDateFromServer(loanId);
+ final LocalDate storedBusinessDate = businessDateHelper.getBusinessLocalDate();
+
+ assertThat(tenantDateBeforeAction).as("Tenant date must be captured before the Working Capital action").isNotNull();
+ assertThat(storedBusinessDate)
+ .as("scenario precondition: the stored business date must differ from the tenant date, otherwise the fallback "
+ + "cannot be told apart from the stored row")
+ .isNotIn(tenantDateBeforeAction, tenantDateAfterAction);
+ assertThat(actual).as(description).isNotNull().isIn(tenantDateBeforeAction, tenantDateAfterAction);
+ }
+
+ private LocalDate getEffectiveDateFromServer(final Long loanId) {
+ return ok(
+ () -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanActionTemplate(loanId, CHARGE_OFF_TEMPLATE))
+ .getChargeOffDate();
+ }
+}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/GlobalConfigurationStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/GlobalConfigurationStepDef.java
index 6fe8d10d47c..36be7b7ca3f 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/GlobalConfigurationStepDef.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/GlobalConfigurationStepDef.java
@@ -49,6 +49,11 @@ public void restoreChargeAccrualDateConfig() {
globalConfigurationHelper.setGlobalConfigValueString("charge-accrual-date", "due-date");
}
+ @After("@BusinessDateDisabledCheck")
+ public void restoreBusinessDateConfig() {
+ globalConfigurationHelper.enableGlobalConfiguration("enable-business-date", 0L);
+ }
+
@Given("Global configuration {string} is disabled")
public void disableGlobalConfiguration(String configKey) {
globalConfigurationHelper.disableGlobalConfiguration(configKey, 0L);
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 dd36d4ebf86..8831c221307 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
@@ -110,6 +110,7 @@
import org.apache.fineract.test.helper.ErrorMessageHelper;
import org.apache.fineract.test.helper.Utils;
import org.apache.fineract.test.helper.WorkingCapitalScheduleMatcher;
+import org.apache.fineract.test.helper.WorkingCapitalTenantDateHelper;
import org.apache.fineract.test.messaging.event.EventCheckHelper;
import org.apache.fineract.test.stepdef.AbstractStepDef;
import org.apache.fineract.test.stepdef.common.JournalEntriesStepDef;
@@ -142,6 +143,7 @@ public class WorkingCapitalLoanAccountStepDef extends AbstractStepDef {
private final EventCheckHelper eventCheckHelper;
private final PaymentTypeResolver paymentTypeResolver;
private final BusinessDateHelper businessDateHelper;
+ private final WorkingCapitalTenantDateHelper workingCapitalTenantDateHelper;
private final JournalEntriesStepDef journalEntriesStepDef;
private final ClientRequestFactory clientRequestFactory;
private final CodeValueResolver codeValueResolver;
@@ -2475,6 +2477,23 @@ public void adminChecksWorkingCapitalPeriodPaymentRateChangesHistoryByExternalId
checkPeriodPaymentRateChangeHistory(data, rateChangesResponse, header, resourceId);
}
+ @Given("Admin captures the current tenant date for the Working Capital loan")
+ public void captureCurrentTenantDateForWorkingCapitalLoan() {
+ workingCapitalTenantDateHelper.captureCurrentTenantDateBeforeAction(getCreatedLoanId());
+ }
+
+ @Then("Working Capital Loan latest period payment rate change was submitted on the current tenant date")
+ public void latestPeriodPaymentRateChangeSubmittedOnTenantDate() {
+ final Long loanId = getCreatedLoanId();
+ final List rateChanges = ok(
+ () -> fineractClient.workingCapitalLoans().getWorkingCapitalLoanRateChangeHistoryById(loanId));
+ final WorkingCapitalLoanPeriodPaymentRateChangeData latest = rateChanges.stream()//
+ .max(Comparator.comparing(WorkingCapitalLoanPeriodPaymentRateChangeData::getId))//
+ .orElseThrow(() -> new IllegalStateException(String.format("No rate change found on loan [%s]", loanId)));
+ workingCapitalTenantDateHelper.assertStampedOnCurrentTenantDate(latest.getSubmittedOnDate(), loanId,
+ String.format("submittedOnDate of latest rate change on loan %s", loanId));
+ }
+
// ====================================
// Private Helper Methods
// ====================================
@@ -4150,6 +4169,8 @@ private List fetchValuesOfRateChangesHistory(List header,
: new Utils.DoubleFormatter(rateChangeData.getNewRate().doubleValue()).format());
case "Reversed" ->
actualValues.add(rateChangeData.getReversed() == null ? null : String.valueOf(rateChangeData.getReversed()));
+ case "Submitted On Date" -> actualValues
+ .add(rateChangeData.getSubmittedOnDate() == null ? null : FORMATTER.format(rateChangeData.getSubmittedOnDate()));
default -> throw new IllegalStateException(String.format("Header name %s cannot be found", headerName));
}
}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalNearBreachActionStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalNearBreachActionStepDef.java
index a87136fb963..978076bbc1a 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalNearBreachActionStepDef.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalNearBreachActionStepDef.java
@@ -26,6 +26,8 @@
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.math.BigDecimal;
+import java.time.format.DateTimeFormatter;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
@@ -35,6 +37,7 @@
import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdNearBreachActionsRequest;
import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse;
import org.apache.fineract.client.models.WorkingCapitalLoanNearBreachActionData;
+import org.apache.fineract.test.helper.WorkingCapitalTenantDateHelper;
import org.apache.fineract.test.stepdef.AbstractStepDef;
import org.apache.fineract.test.support.TestContextKey;
@@ -42,7 +45,10 @@
@RequiredArgsConstructor
public class WorkingCapitalNearBreachActionStepDef extends AbstractStepDef {
+ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd MMMM yyyy");
+
private final FineractFeignClient fineractClient;
+ private final WorkingCapitalTenantDateHelper workingCapitalTenantDateHelper;
@When("Admin creates a near breach reschedule action with threshold {string} frequency {int} frequencyType {string}")
public void createNearBreachRescheduleAction(final String threshold, final int frequency, final String frequencyType) {
@@ -114,10 +120,25 @@ private void verifyActionField(final WorkingCapitalLoanNearBreachActionData actu
assertThat(actual.getFrequency()).as("Frequency for row %d", rowNumber).isEqualTo(Integer.parseInt(expectedValue));
case "frequencyType" ->
assertThat(actual.getFrequencyType()).as("FrequencyType for row %d", rowNumber).isEqualTo(expectedValue);
+ case "submittedOnDate" -> {
+ assertThat(actual.getSubmittedOnDate()).as("SubmittedOnDate for row %d", rowNumber).isNotNull();
+ assertThat(FORMATTER.format(actual.getSubmittedOnDate())).as("SubmittedOnDate for row %d", rowNumber)
+ .isEqualTo(expectedValue);
+ }
default -> throw new IllegalArgumentException("Unknown near breach action field: " + fieldName);
}
}
+ @Then("Latest near breach action was submitted on the current tenant date")
+ public void latestNearBreachActionSubmittedOnTenantDate() {
+ final Long loanId = extractLoanId();
+ final WorkingCapitalLoanNearBreachActionData latest = retrieveNearBreachActionHistory(loanId).stream()//
+ .max(Comparator.comparing(WorkingCapitalLoanNearBreachActionData::getId))//
+ .orElseThrow(() -> new IllegalStateException(String.format("No near breach action found on loan [%s]", loanId)));
+ workingCapitalTenantDateHelper.assertStampedOnCurrentTenantDate(latest.getSubmittedOnDate(), loanId,
+ String.format("submittedOnDate of latest near breach action on loan %d", loanId));
+ }
+
private PostWorkingCapitalLoansLoanIdNearBreachActionsRequest buildRequest(final String threshold, final int frequency,
final String frequencyType) {
return new PostWorkingCapitalLoansLoanIdNearBreachActionsRequest()
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
index e3b4b711486..a97250de23a 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
@@ -375,6 +375,7 @@ public abstract class TestContextKey {
public static final String WORKING_CAPITAL_NEAR_BREACH_CREATE_REQUEST_FOR_UPDATE = "workingCapitalNearBreachCreateRequestForUpdate";
public static final String WC_LOAN_ACTION_TEMPLATE_RESPONSE = "wcLoanActionTemplateResponse";
public static final String WORKING_CAPITAL_LOAN_RATE_CHANGE_ID = "wcLoanRateChangeId";
+ public static final String WORKING_CAPITAL_CURRENT_TENANT_DATE_BEFORE_ACTION = "wcCurrentTenantDateBeforeAction";
public static final String WORKING_CAPITAL_CHARGE_ID = "workingCapitalChargeId";
public static final String WORKING_CAPITAL_LOAN_CHARGE_IDS = "workingCapitalLoanChargeIds";
public static final String WORKING_CAPITAL_CHARGE_TEMPLATE = "workingCapitalChargeTemplate";
diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature
index 96e8c4c49da..ede5c52ea0a 100644
--- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature
+++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalNearBreachEvaluation.feature
@@ -1194,3 +1194,30 @@ Feature: Working Capital Near Breach Evaluation
| 1 | 2026-01-01 | 2026-02-11 | 42 | 250.00 | 250.00 | true | null |
Then Admin closes the Working Capital loan with all obligations met with a full repayment on "06 February 2026"
+ @TestRailId:C98199
+ Scenario: Verify near breach action submitted on date follows the business date and stays immutable
+ Given Global configuration "enable-business-date" is enabled
+ 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 Product with breach and near breach config and overrides enabled:
+ | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays |
+ | 9 | DAYS | FLAT | 90 | 3 | DAYS | 33.33 | |
+ And Admin creates a working capital loan using created product with the following data:
+ | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount |
+ | 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"
+ When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount
+ #--- submitted on date is stamped with the business date in force at creation ---#
+ When Admin sets the business date to "02 January 2026"
+ And Admin creates a near breach reschedule action with threshold "50" frequency 3 frequencyType "DAYS"
+ Then Near breach action history has the following data:
+ | action | threshold | frequency | frequencyType | submittedOnDate |
+ | RESCHEDULE | 50 | 3 | DAYS | 02 January 2026 |
+ #--- a later business date stamps only the new record, the earlier one is immutable ---#
+ When Admin sets the business date to "04 January 2026"
+ And Admin creates a near breach reschedule action with threshold "60" frequency 5 frequencyType "DAYS"
+ Then Near breach action history has the following data:
+ | action | threshold | frequency | frequencyType | submittedOnDate |
+ | RESCHEDULE | 60 | 5 | DAYS | 04 January 2026 |
+ | RESCHEDULE | 50 | 3 | DAYS | 02 January 2026 |
+ Then Admin closes the Working Capital loan with all obligations met with a full repayment on "04 January 2026"
diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalPeriodPaymentRate.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalPeriodPaymentRate.feature
index 7eaa24c8c2b..d27a420df8b 100644
--- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalPeriodPaymentRate.feature
+++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalPeriodPaymentRate.feature
@@ -1956,3 +1956,29 @@ Feature: Working Capital Period Payment Rate
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPaymentVolume | periodPaymentRate | discount |
| WCLP_ADVANCED_ACCOUNTING | 2026-01-01 | 2026-01-01 | Active | 1100.0 | 1000.0 | 100000.0 | 18.0 | 100.0 |
Then Admin closes the Working Capital loan with a full repayment on "01 February 2026"
+
+ @TestRailId:C98201
+ Scenario: Verify Working Capital period payment rate change submitted on date follows the business date and stays immutable - UC24
+ 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 | totalPaymentVolume | periodPaymentRate | discount |
+ | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 |
+ Then Working capital loan creation was successful
+ 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"
+ #--- submitted on date is stamped with the business date in force at creation ---#
+ When Admin sets the business date to "10 January 2026"
+ And Admin update Working Capital period payment rate with "12.5" value
+ Then Working Capital Loan Period Payment Rate changes history contains the following data:
+ | Effective Date | Previous Rate | New Rate | Reversed | Submitted On Date |
+ | 10 January 2026 | 1.0 | 12.5 | false | 10 January 2026 |
+ #--- a later business date stamps only the new record, the earlier one is immutable ---#
+ When Admin sets the business date to "20 January 2026"
+ And Admin update Working Capital period payment rate with "15" value
+ Then Working Capital Loan Period Payment Rate changes history contains the following data:
+ | Effective Date | Previous Rate | New Rate | Reversed | Submitted On Date |
+ | 10 January 2026 | 1.0 | 12.5 | false | 10 January 2026 |
+ | 20 January 2026 | 12.5 | 15.0 | false | 20 January 2026 |
+ Then Admin closes the Working Capital loan with a full repayment on "20 January 2026"
diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalSubmittedOnDateBusinessDateDisabled.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalSubmittedOnDateBusinessDateDisabled.feature
new file mode 100644
index 00000000000..0a05c273fe3
--- /dev/null
+++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalSubmittedOnDateBusinessDateDisabled.feature
@@ -0,0 +1,41 @@
+@WorkingCapital
+@WorkingCapitalSubmittedOnDateBusinessDateDisabledFeature
+@BusinessDateDisabledCheck
+Feature: Working Capital submitted on date when business date is disabled
+
+ @TestRailId:C98202
+ Scenario: Verify Working Capital period payment rate change submitted on date falls back to the system date when business date is disabled - UC25
+ Given Global configuration "enable-business-date" is enabled
+ 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 | totalPaymentVolume | periodPaymentRate | discount |
+ | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 |
+ Then Working capital loan creation was successful
+ 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"
+ #--- with the config off the stamp must be the machine date, not the stored business date ---#
+ Given Global configuration "enable-business-date" is disabled
+ And Admin captures the current tenant date for the Working Capital loan
+ When Admin update Working Capital period payment rate with "12.5" value
+ Then Working Capital Loan latest period payment rate change was submitted on the current tenant date
+
+ @TestRailId:C98200
+ Scenario: Verify near breach action submitted on date falls back to the system date when business date is disabled
+ Given Global configuration "enable-business-date" is enabled
+ 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 Product with breach and near breach config and overrides enabled:
+ | breachFrequency | breachFrequencyType | breachAmountCalculationType | breachAmount | nearBreachFrequency | nearBreachFrequencyType | nearBreachThreshold | delinquencyGraceDays |
+ | 9 | DAYS | FLAT | 90 | 3 | DAYS | 33.33 | |
+ And Admin creates a working capital loan using created product with the following data:
+ | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount |
+ | 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"
+ When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount
+ #--- with the config off the stamp must be the machine date, not the stored business date ---#
+ Given Global configuration "enable-business-date" is disabled
+ And Admin captures the current tenant date for the Working Capital loan
+ When Admin creates a near breach reschedule action with threshold "40" frequency 4 frequencyType "DAYS"
+ Then Latest near breach action was submitted on the current tenant date
diff --git a/fineract-e2e-tests-runner/src/test/resources/junit-platform.properties b/fineract-e2e-tests-runner/src/test/resources/junit-platform.properties
index a0531952e4d..4199489e9b3 100644
--- a/fineract-e2e-tests-runner/src/test/resources/junit-platform.properties
+++ b/fineract-e2e-tests-runner/src/test/resources/junit-platform.properties
@@ -33,4 +33,8 @@ cucumber.execution.exclusive-resources.isolated.read-write=org.junit.platform.en
# that relies on due-date between its charge-add and COB steps. Features tagged @SerialChargeAccrualConfig acquire this
# resource in read-write mode and therefore never run concurrently with each other.
cucumber.execution.exclusive-resources.SerialChargeAccrualConfig.read-write=charge-accrual-date-config
+# Scenarios tagged @BusinessDateDisabledCheck switch the tenant-wide "enable-business-date" configuration off. Every
+# business-date-dependent scenario in the suite would observe that, and none of them carries a tag, so a named resource
+# (which only serializes tagged scenarios against each other) would not protect them.
+cucumber.execution.exclusive-resources.BusinessDateDisabledCheck.read-write=org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY
cucumber.execution.execution-mode.feature=same_thread
diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanNearBreachActionData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanNearBreachActionData.java
index 26f5cbf2260..de34fcc8b20 100644
--- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanNearBreachActionData.java
+++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanNearBreachActionData.java
@@ -18,11 +18,15 @@
*/
package org.apache.fineract.portfolio.workingcapitalloan.data;
+import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
+import java.time.LocalDate;
import java.time.OffsetDateTime;
import org.apache.fineract.portfolio.workingcapitalloan.domain.NearBreachActionType;
public record WorkingCapitalLoanNearBreachActionData(Long id, Long loanId, NearBreachActionType action, BigDecimal threshold,
- Integer frequency, String frequencyType, OffsetDateTime createdDate) {
+ Integer frequency, String frequencyType,
+ @Schema(deprecated = true, description = "Audit/system timestamp. Prefer submittedOnDate for the booking business/tenant date.") //
+ OffsetDateTime createdDate, LocalDate submittedOnDate) {
}
diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanPeriodPaymentRateChangeData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanPeriodPaymentRateChangeData.java
index 1046c9f84c4..67af3f21d1d 100644
--- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanPeriodPaymentRateChangeData.java
+++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanPeriodPaymentRateChangeData.java
@@ -18,11 +18,14 @@
*/
package org.apache.fineract.portfolio.workingcapitalloan.data;
+import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
public record WorkingCapitalLoanPeriodPaymentRateChangeData(Long id, Long loanId, LocalDate effectiveDate, BigDecimal previousRate,
- BigDecimal newRate, boolean reversed, LocalDate reversedOnDate, OffsetDateTime createdDate) {
+ BigDecimal newRate, boolean reversed, LocalDate reversedOnDate,
+ @Schema(deprecated = true, description = "Audit/system timestamp. Prefer submittedOnDate for the booking business/tenant date.") //
+ OffsetDateTime createdDate, LocalDate submittedOnDate) {
}
diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanNearBreachAction.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanNearBreachAction.java
index 76f94347920..305d2f9af6d 100644
--- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanNearBreachAction.java
+++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanNearBreachAction.java
@@ -27,10 +27,12 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
+import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
@Getter
@Setter
@@ -57,6 +59,9 @@ public class WorkingCapitalLoanNearBreachAction extends AbstractAuditableWithUTC
@Column(name = "frequency_type")
private WorkingCapitalLoanPeriodFrequencyType frequencyType;
+ @Column(name = "submitted_on_date")
+ private LocalDate submittedOnDate;
+
public static WorkingCapitalLoanNearBreachAction create(final WorkingCapitalLoan loan, final NearBreachActionType action,
final BigDecimal threshold, final Integer frequency, final WorkingCapitalLoanPeriodFrequencyType frequencyType) {
final WorkingCapitalLoanNearBreachAction entity = new WorkingCapitalLoanNearBreachAction();
@@ -65,6 +70,7 @@ public static WorkingCapitalLoanNearBreachAction create(final WorkingCapitalLoan
entity.threshold = threshold;
entity.frequency = frequency;
entity.frequencyType = frequencyType;
+ entity.submittedOnDate = DateUtils.getBusinessLocalDate();
return entity;
}
}
diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanPeriodPaymentRateChange.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanPeriodPaymentRateChange.java
index 71957075f81..f09b67ebe7b 100644
--- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanPeriodPaymentRateChange.java
+++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanPeriodPaymentRateChange.java
@@ -31,6 +31,7 @@
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
@Getter
@Setter
@@ -58,6 +59,9 @@ public class WorkingCapitalLoanPeriodPaymentRateChange extends AbstractAuditable
@Column(name = "reversed_on_date")
private LocalDate reversedOnDate;
+ @Column(name = "submitted_on_date")
+ private LocalDate submittedOnDate;
+
@Version
private int version;
@@ -69,6 +73,7 @@ public static WorkingCapitalLoanPeriodPaymentRateChange create(final WorkingCapi
change.previousRate = previousRate;
change.newRate = newRate;
change.reversed = false;
+ change.submittedOnDate = DateUtils.getBusinessLocalDate();
return change;
}
diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanPeriodPaymentRateChangeReadServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanPeriodPaymentRateChangeReadServiceImpl.java
index 3f1372536e1..04d18c27085 100644
--- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanPeriodPaymentRateChangeReadServiceImpl.java
+++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanPeriodPaymentRateChangeReadServiceImpl.java
@@ -48,6 +48,6 @@ private WorkingCapitalLoanPeriodPaymentRateChangeData toData(final WorkingCapita
final Long loanId) {
return new WorkingCapitalLoanPeriodPaymentRateChangeData(entity.getId(), loanId, entity.getEffectiveDate(),
entity.getPreviousRate(), entity.getNewRate(), entity.isReversed(), entity.getReversedOnDate(),
- entity.getCreatedDate().orElse(null));
+ entity.getCreatedDate().orElse(null), entity.getSubmittedOnDate());
}
}
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 71b13775f6a..b0b3cb71c8d 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
@@ -92,4 +92,6 @@
+
+
diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0071_wc_loan_period_payment_rate_change_submitted_on_date.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0071_wc_loan_period_payment_rate_change_submitted_on_date.xml
new file mode 100644
index 00000000000..39c8c37f975
--- /dev/null
+++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0071_wc_loan_period_payment_rate_change_submitted_on_date.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ submitted_on_date IS NULL
+
+
+
diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0072_wc_loan_near_breach_action_submitted_on_date.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0072_wc_loan_near_breach_action_submitted_on_date.xml
new file mode 100644
index 00000000000..7df9b483f4c
--- /dev/null
+++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0072_wc_loan_near_breach_action_submitted_on_date.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ submitted_on_date IS NULL
+
+
+
diff --git a/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalSubmittedOnDateTest.java b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalSubmittedOnDateTest.java
new file mode 100644
index 00000000000..b0bd7ec91aa
--- /dev/null
+++ b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalSubmittedOnDateTest.java
@@ -0,0 +1,147 @@
+/**
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import org.apache.fineract.infrastructure.businessdate.data.service.BusinessDateDTO;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDate;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateRepository;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.businessdate.mapper.BusinessDateMapper;
+import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformServiceImpl;
+import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
+import org.apache.fineract.infrastructure.core.domain.ActionContext;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class WorkingCapitalSubmittedOnDateTest {
+
+ private static final LocalDate CONFIGURED_BUSINESS_DATE = LocalDate.of(2019, 3, 14);
+ private static final LocalDate RATE_EFFECTIVE_DATE = LocalDate.of(2018, 6, 1);
+
+ @Mock
+ private ConfigurationDomainService configurationDomainService;
+
+ @Mock
+ private BusinessDateRepository repository;
+
+ @Mock
+ private BusinessDateMapper businessDateMapper;
+
+ @InjectMocks
+ private BusinessDateReadPlatformServiceImpl businessDateReadPlatformService;
+
+ @BeforeEach
+ void setUp() {
+ ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "UTC", null));
+ ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+ }
+
+ @AfterEach
+ void tearDown() {
+ ThreadLocalContextUtil.reset();
+ }
+
+ @Test
+ void periodPaymentRateChangeSubmittedOnDateIsConfiguredDateWhenBusinessDateEnabled() {
+ givenBusinessDateEnabled(CONFIGURED_BUSINESS_DATE);
+ loadResolvedDatesOntoRequestContext();
+
+ final WorkingCapitalLoanPeriodPaymentRateChange change = createPeriodPaymentRateChange();
+
+ assertThat(change.getSubmittedOnDate()).isEqualTo(CONFIGURED_BUSINESS_DATE);
+ assertThat(change.getSubmittedOnDate()).isNotEqualTo(DateUtils.getLocalDateOfTenant());
+ }
+
+ @Test
+ void periodPaymentRateChangeSubmittedOnDateIsTenantDateWhenBusinessDateDisabled() {
+ givenBusinessDateDisabled();
+ loadResolvedDatesOntoRequestContext();
+
+ final WorkingCapitalLoanPeriodPaymentRateChange change = createPeriodPaymentRateChange();
+
+ assertThat(change.getSubmittedOnDate()).isEqualTo(DateUtils.getLocalDateOfTenant());
+ verify(repository, never()).findAllBusinessDates();
+ }
+
+ @Test
+ void nearBreachActionSubmittedOnDateIsConfiguredDateWhenBusinessDateEnabled() {
+ givenBusinessDateEnabled(CONFIGURED_BUSINESS_DATE);
+ loadResolvedDatesOntoRequestContext();
+
+ final WorkingCapitalLoanNearBreachAction action = createNearBreachAction();
+
+ assertThat(action.getSubmittedOnDate()).isEqualTo(CONFIGURED_BUSINESS_DATE);
+ assertThat(action.getSubmittedOnDate()).isNotEqualTo(DateUtils.getLocalDateOfTenant());
+ }
+
+ @Test
+ void nearBreachActionSubmittedOnDateIsTenantDateWhenBusinessDateDisabled() {
+ givenBusinessDateDisabled();
+ loadResolvedDatesOntoRequestContext();
+
+ final WorkingCapitalLoanNearBreachAction action = createNearBreachAction();
+
+ assertThat(action.getSubmittedOnDate()).isEqualTo(DateUtils.getLocalDateOfTenant());
+ verify(repository, never()).findAllBusinessDates();
+ }
+
+ private void givenBusinessDateEnabled(final LocalDate configuredBusinessDate) {
+ final List stored = List.of(mock(BusinessDate.class));
+ given(configurationDomainService.isBusinessDateEnabled()).willReturn(true);
+ given(repository.findAllBusinessDates()).willReturn(stored);
+ given(businessDateMapper.mapEntity(stored))
+ .willReturn(List.of(BusinessDateDTO.builder().type(BusinessDateType.BUSINESS_DATE).date(configuredBusinessDate).build()));
+ }
+
+ private void givenBusinessDateDisabled() {
+ given(configurationDomainService.isBusinessDateEnabled()).willReturn(false);
+ }
+
+ private void loadResolvedDatesOntoRequestContext() {
+ ThreadLocalContextUtil.setBusinessDates(businessDateReadPlatformService.getBusinessDates());
+ }
+
+ private static WorkingCapitalLoanPeriodPaymentRateChange createPeriodPaymentRateChange() {
+ return WorkingCapitalLoanPeriodPaymentRateChange.create(mock(WorkingCapitalLoan.class), RATE_EFFECTIVE_DATE, BigDecimal.ONE,
+ new BigDecimal("12.5"));
+ }
+
+ private static WorkingCapitalLoanNearBreachAction createNearBreachAction() {
+ return WorkingCapitalLoanNearBreachAction.create(mock(WorkingCapitalLoan.class), NearBreachActionType.RESCHEDULE,
+ new BigDecimal("10"), 7, WorkingCapitalLoanPeriodFrequencyType.DAYS);
+ }
+}