Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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 java.time.format.DateTimeFormatter;
Expand Down Expand Up @@ -78,6 +79,35 @@ public LocalDate getBusinessLocalDate() {
return ok(() -> fineractClient.businessDateManagement().getBusinessDate(BUSINESS_DATE, Map.of())).getDate();
}

/**
* Captures the server's effective date immediately before an action that should use the tenant date. The business
* date API cannot be used for this because it returns the persisted business-date row even when business date is
* disabled. The read-only charge-off template exposes a date resolved through
* {@code DateUtils.getBusinessLocalDate} and therefore returns the tenant date while the configuration is disabled.
*/
public void captureCurrentTenantDateBeforeWorkingCapitalAction(final Long loanId) {
TestContext.INSTANCE.set(TestContextKey.WORKING_CAPITAL_CURRENT_TENANT_DATE_BEFORE_ACTION,
getEffectiveWorkingCapitalDateFromServer(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.
*/
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 = getEffectiveWorkingCapitalDateFromServer(loanId);
final LocalDate storedBusinessDate = getBusinessLocalDate();

assertThat(tenantDateBeforeAction).as("Tenant date must be captured before the Working Capital action").isNotNull();
assertThat(actual).as(description).isNotNull().isNotEqualTo(storedBusinessDate).isIn(tenantDateBeforeAction, tenantDateAfterAction);
}

private LocalDate getEffectiveWorkingCapitalDateFromServer(final Long loanId) {
return ok(() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanActionTemplate(loanId, "chargeOff"))
.getChargeOffDate();
}

public String getBusinessDate() {
log.debug("Getting business date (using Feign)");
BusinessDateResponse response = ok(() -> fineractClient.businessDateManagement().getBusinessDate(BUSINESS_DATE, Map.of()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2460,6 +2460,23 @@ public void adminChecksWorkingCapitalPeriodPaymentRateChangesHistoryByExternalId
checkPeriodPaymentRateChangeHistory(data, rateChangesResponse, header, resourceId);
}

@Given("Admin captures the current tenant date for the Working Capital loan")
public void captureCurrentTenantDateForWorkingCapitalLoan() {
businessDateHelper.captureCurrentTenantDateBeforeWorkingCapitalAction(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<WorkingCapitalLoanPeriodPaymentRateChangeData> 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)));
businessDateHelper.assertStampedOnCurrentTenantDate(latest.getSubmittedOnDate(), loanId,
String.format("submittedOnDate of latest rate change on loan %s", loanId));
}

// ====================================
// Private Helper Methods
// ====================================
Expand Down Expand Up @@ -4135,6 +4152,8 @@ private List<String> fetchValuesOfRateChangesHistory(List<String> 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));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,14 +37,18 @@
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.BusinessDateHelper;
import org.apache.fineract.test.stepdef.AbstractStepDef;
import org.apache.fineract.test.support.TestContextKey;

@Slf4j
@RequiredArgsConstructor
public class WorkingCapitalNearBreachActionStepDef extends AbstractStepDef {

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd MMMM yyyy");

private final FineractFeignClient fineractClient;
private final BusinessDateHelper businessDateHelper;

@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) {
Expand Down Expand Up @@ -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)));
businessDateHelper.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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
package org.apache.fineract.portfolio.workingcapitalloan.data;

import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.time.LocalDate;
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, LocalDate submittedOnDate) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@

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, LocalDate submittedOnDate) {

}
Loading
Loading