From 027c4948ab0dd4e2e4c9382ecf5a08e652de0fdc Mon Sep 17 00:00:00 2001 From: mariiaKraievska Date: Thu, 20 Aug 2026 11:58:06 +0300 Subject: [PATCH 1/2] FINERACT-2455: WC - created and submitted date of all features supported in WCP should follow business date or system date based on configuration --- ...orkingCapitalLoanNearBreachActionData.java | 6 +- ...apitalLoanPeriodPaymentRateChangeData.java | 5 +- .../WorkingCapitalLoanNearBreachAction.java | 6 + ...ingCapitalLoanPeriodPaymentRateChange.java | 5 + ...eriodPaymentRateChangeReadServiceImpl.java | 2 +- .../module-changelog-master.xml | 2 + ..._payment_rate_change_submitted_on_date.xml | 49 ++++++ ...n_near_breach_action_submitted_on_date.xml | 49 ++++++ .../WorkingCapitalSubmittedOnDateTest.java | 147 ++++++++++++++++++ 9 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0071_wc_loan_period_payment_rate_change_submitted_on_date.xml create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0072_wc_loan_near_breach_action_submitted_on_date.xml create mode 100644 fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalSubmittedOnDateTest.java 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); + } +} From 0214bb3c40140e71b13a57007b41473c6b8518c7 Mon Sep 17 00:00:00 2001 From: Rustam Zeinalov Date: Fri, 21 Aug 2026 15:27:13 +0200 Subject: [PATCH 2/2] FINERACT-2455: added e2e tests for created and submitted date of all features supported in WCP should follow business date or system date based on configuration --- .../WorkingCapitalTenantDateHelper.java | 81 +++++++++++++++++++ .../common/GlobalConfigurationStepDef.java | 5 ++ .../WorkingCapitalLoanAccountStepDef.java | 21 +++++ ...WorkingCapitalNearBreachActionStepDef.java | 21 +++++ .../fineract/test/support/TestContextKey.java | 1 + ...WorkingCapitalNearBreachEvaluation.feature | 27 +++++++ .../WorkingCapitalPeriodPaymentRate.feature | 26 ++++++ ...ubmittedOnDateBusinessDateDisabled.feature | 41 ++++++++++ .../test/resources/junit-platform.properties | 4 + 9 files changed, 227 insertions(+) create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalTenantDateHelper.java create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalSubmittedOnDateBusinessDateDisabled.feature 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