FINERACT-2258: Fix for making the Post Interest for Savings job properly work in a multitenant environment
Fineract Docker build - MariaDB / build (push) Canceled after 0s
Fineract Docker build - PostgreSQL / build (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract ActiveMQ Smoke / build (push) Canceled after 0s
Fineract Build Progressive Loan Embeddable Jar & Test with a Sample Application / build (push) Canceled after 0s
Fineract Build & Test - MariaDB / build (push) Canceled after 0s
Fineract Build & Test - MySQL / build (push) Canceled after 0s
Fineract Build & Test - PostgreSQL / build (push) Canceled after 0s
Fineract Tests / build (push) Canceled after 0s
Fineract Kafka Smoke / build (push) Canceled after 0s

This commit is contained in:
Arnold Galovics
2025-04-10 11:01:48 +02:00
committed by Arnold Gálovics
parent 09879800e0
commit 872f97b6b6
20 changed files with 611 additions and 75 deletions
@@ -18,18 +18,19 @@
*/
package org.apache.fineract.integrationtests;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import org.apache.fineract.client.models.DeleteSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.GetSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdRequest;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.PostSavingsAccountsRequest;
import org.apache.fineract.client.models.PostSavingsAccountsResponse;
import org.apache.fineract.client.models.PutSavingsAccountsAccountIdRequest;
import org.apache.fineract.client.models.PutSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.SavingsAccountData;
import org.apache.fineract.client.util.Calls;
import org.apache.fineract.integrationtests.client.IntegrationTest;
import org.junit.jupiter.api.Order;
@@ -100,13 +101,12 @@ public class SavingsAccountsExternalIdTest extends IntegrationTest {
request.dateFormat(dateFormat);
request.setLocale(locale);
request.setActivatedOnDate(formattedDate);
Response<GetSavingsAccountsAccountIdResponse> response = okR(
fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all"));
Response<SavingsAccountData> response = okR(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, null, "all"));
assertThat(response.isSuccessful()).isTrue();
assertThat(response.body()).isNotNull();
assertThat(response.body().getStatus().getCode()).isEqualTo("savingsAccountStatusType.approved");
assertThat(response.body().getNominalAnnualInterestRate()).isEqualTo(5.999);
assertThat(response.body().getNominalAnnualInterestRate()).isEqualByComparingTo(BigDecimal.valueOf(5.999));
}
@Test
@@ -129,8 +129,7 @@ public class SavingsAccountsExternalIdTest extends IntegrationTest {
request.dateFormat(dateFormat);
request.setLocale(locale);
request.setActivatedOnDate(formattedDate);
Response<GetSavingsAccountsAccountIdResponse> response = okR(
fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all"));
Response<SavingsAccountData> response = okR(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, null, "all"));
assertThat(response.isSuccessful()).isTrue();
assertThat(response.body()).isNotNull();
@@ -159,8 +158,8 @@ public class SavingsAccountsExternalIdTest extends IntegrationTest {
request.dateFormat(dateFormat);
request.setLocale(locale);
request.setActivatedOnDate(formattedDate);
Response<GetSavingsAccountsAccountIdResponse> response = Calls
.executeU(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all"));
Response<SavingsAccountData> response = Calls
.executeU(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, null, "all"));
assertThat(response.raw().code()).isEqualTo(404);
}
@@ -0,0 +1,108 @@
/**
* 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.integrationtests.savings;
import java.math.BigDecimal;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import org.apache.fineract.client.models.PostSavingsProductsRequest;
import org.apache.fineract.client.models.PostSavingsProductsResponse;
import org.apache.fineract.client.models.SavingsAccountData;
import org.apache.fineract.client.models.SavingsAccountTransactionData;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.savings.base.BaseSavingsIntegrationTest;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
@Order(2)
public class SavingsInterestPostingTest extends BaseSavingsIntegrationTest {
@Test
public void testSavingsInterestPosting_Works_ForMultipleAccounts() {
List<AccountWithDepositAmount> accounts = new ArrayList<>();
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create product
PostSavingsProductsRequest productsRequest = dailyInterestPostingProduct();
PostSavingsProductsResponse product = createProduct(productsRequest);
Integer productId = product.getResourceId();
// Create accounts
IntStream.range(0, 200)//
.parallel().mapToObj(i -> applySavingsRequest(clientId, productId.longValue(), "01 January 2023"))//
.map(this::applySavingsAccount) //
.mapToLong(savingsAccount -> savingsAccount.getResourceId().longValue()) //
.forEach(savingsId -> {
approveSavingsAccount(savingsId, "01 January 2023");
activateSavingsAccount(savingsId, "01 January 2023");
accounts.add(new AccountWithDepositAmount(savingsId));
});
});
runFromToInclusive("02 January 2023", "10 January 2023", (date) -> {
SecureRandom rnd = new SecureRandom();
accounts.parallelStream().forEach(account -> {
long lAmount = rnd.nextLong(10_000_000L);
BigDecimal amount = BigDecimal.valueOf(lAmount);
deposit(account.savingsId, date, amount);
account.addDeposit(amount);
});
});
runAt("11 January 2023", () -> {
String jobName = "Post Interest For Savings";
schedulerJobHelper.executeAndAwaitJob(jobName);
accounts.parallelStream().forEach(account -> {
SavingsAccountData accountData = getSavingsAccount(account.savingsId);
BigDecimal accountBalance = accountData.getSummary().getAccountBalance();
BigDecimal accountBalanceByTransactions = accountData.getTransactions().stream()
.map(SavingsAccountTransactionData::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
assertThat(accountBalance)
.withFailMessage("Account balance (%s) and calculated balance (%s) from transactions do not match", accountBalance,
accountBalanceByTransactions)
.isEqualByComparingTo(accountBalanceByTransactions);
BigDecimal summedDepositAmount = accountData.getTransactions().stream().filter(tx -> tx.getTransactionType().getDeposit())
.map(SavingsAccountTransactionData::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
assertThat(summedDepositAmount).withFailMessage(
"Calculated balance (%s) from deposit transactions and submitted deposit balance (%s) do not match", accountBalance,
summedDepositAmount).isEqualByComparingTo(account.depositedAmount);
});
});
}
static class AccountWithDepositAmount {
public Long savingsId;
public BigDecimal depositedAmount = BigDecimal.ZERO;
AccountWithDepositAmount(Long savingsId) {
this.savingsId = savingsId;
}
public void addDeposit(BigDecimal amount) {
depositedAmount = depositedAmount.add(amount);
}
}
}
@@ -0,0 +1,249 @@
/**
* 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.integrationtests.savings.base;
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import lombok.AllArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.BusinessDateRequest;
import org.apache.fineract.client.models.PostSavingsAccountTransactionsRequest;
import org.apache.fineract.client.models.PostSavingsAccountTransactionsResponse;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdRequest;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.PostSavingsAccountsRequest;
import org.apache.fineract.client.models.PostSavingsAccountsResponse;
import org.apache.fineract.client.models.PostSavingsProductsRequest;
import org.apache.fineract.client.models.PostSavingsProductsResponse;
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
import org.apache.fineract.client.models.SavingsAccountData;
import org.apache.fineract.client.models.SavingsAccountTransactionData;
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
import org.apache.fineract.integrationtests.client.IntegrationTest;
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
import org.apache.fineract.integrationtests.common.system.CodeHelper;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
@Slf4j
public class BaseSavingsIntegrationTest extends IntegrationTest {
protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
static {
Utils.initializeRESTAssured();
}
protected final ResponseSpecification responseSpec = createResponseSpecification(Matchers.is(200));
private final String fullAdminAuthKey = getFullAdminAuthKey();
protected final RequestSpecification requestSpec = createRequestSpecification(fullAdminAuthKey);
protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
protected GlobalConfigurationHelper globalConfigurationHelper = new GlobalConfigurationHelper();
protected final CodeHelper codeHelper = new CodeHelper();
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
protected ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
protected SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(requestSpec);
protected void runFromToInclusive(String fromDate, String toDate, Consumer<String> runnable) {
DateTimeFormatter format = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
LocalDate startDate = LocalDate.parse(fromDate, format);
LocalDate endDate = LocalDate.parse(toDate, format);
LocalDate currentDate = startDate;
while (currentDate.isBefore(endDate) || currentDate.isEqual(endDate)) {
runAt(format.format(currentDate), runnable);
currentDate = currentDate.plusDays(1);
}
}
protected void runAt(String date, Runnable runnable) {
runAt(date, (d) -> runnable.run());
}
protected void runAt(String date, Consumer<String> runnable) {
try {
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(true));
businessDateHelper.updateBusinessDate(
new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
runnable.accept(date);
} finally {
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(false));
}
}
protected PostSavingsProductsRequest dailyInterestPostingProduct() {
return new PostSavingsProductsRequest().locale("en").name(Utils.uniqueRandomStringGenerator("DAILY_INTEREST", 6))//
.shortName(Utils.uniqueRandomStringGenerator("", 4))//
.description("Daily interest posting product") //
.nominalAnnualInterestRate(10.0) //
.digitsAfterDecimal(0) //
.currencyCode("EUR") //
.accountingRule(1) // none
.interestCalculationDaysInYearType(DaysInYearType.DAYS_365).interestCompoundingPeriodType(InterestPeriodType.DAILY)
.interestCalculationType(InterestCalculationType.AVERAGE_DAILY_BALANCE) //
.interestPostingPeriodType(InterestPeriodType.DAILY);//
}
protected PostSavingsProductsResponse createProduct(PostSavingsProductsRequest productsRequest) {
return ok(fineractClient().savingsProducts.create13(productsRequest));
}
protected PostSavingsAccountsRequest applySavingsRequest(Long clientId, Long productId, String submittedDate) {
return new PostSavingsAccountsRequest().clientId(clientId.intValue()).productId(productId.intValue()).dateFormat(DATETIME_PATTERN)
.locale("en").submittedOnDate(submittedDate);
}
protected PostSavingsAccountsResponse applySavingsAccount(PostSavingsAccountsRequest request) {
return ok(fineractClient().savingsAccounts.submitApplication2(request));
}
protected PostSavingsAccountsAccountIdResponse approveSavingsAccount(Long savingsId, String date) {
PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest().dateFormat(DATETIME_PATTERN).locale("en")
.approvedOnDate(date);
return ok(fineractClient().savingsAccounts.handleCommands6(savingsId, request, "approve"));
}
protected PostSavingsAccountsAccountIdResponse activateSavingsAccount(Long savingsId, String date) {
PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest().dateFormat(DATETIME_PATTERN).locale("en")
.activatedOnDate(date);
return ok(fineractClient().savingsAccounts.handleCommands6(savingsId, request, "activate"));
}
protected PostSavingsAccountTransactionsResponse deposit(Long savingsId, String date, BigDecimal amount) {
PostSavingsAccountTransactionsRequest request = new PostSavingsAccountTransactionsRequest() //
.dateFormat(DATETIME_PATTERN) //
.locale("en") //
.paymentTypeId(1).transactionAmount(amount) //
.transactionDate(date); //
return ok(fineractClient().savingsTransactions.transaction2(savingsId, request, "deposit"));
}
protected SavingsAccountData getSavingsAccount(Long savingsId) {
return ok(fineractClient().savingsAccounts.retrieveOne25(savingsId, false, null, "transactions"));
}
protected List<SavingsAccountTransactionData> getTransactions(Long savingsId) {
return ok(fineractClient().savingsAccounts.retrieveOne25(savingsId, false, null, "transactions")).getTransactions();
}
protected void verifyNoTransactions(Long savingsId) {
verifyTransactions(savingsId, (Transaction[]) null);
}
protected void verifyTransactions(Long savingsId, Transaction... transactions) {
SavingsAccountData savingsDetails = ok(fineractClient().savingsAccounts.retrieveOne25(savingsId, false, null, "transactions"));
if (transactions == null || transactions.length == 0) {
Assertions.assertTrue(savingsDetails.getTransactions().isEmpty(), "No transaction is expected on savings account " + savingsId);
} else {
Assertions.assertEquals(transactions.length, savingsDetails.getTransactions().size());
Arrays.stream(transactions).forEach(tr -> {
Optional<SavingsAccountTransactionData> optTx = savingsDetails.getTransactions().stream()
.filter(item -> Objects.requireNonNull(item.getAmount()).compareTo(BigDecimal.valueOf(tr.amount)) == 0 //
&& Objects.equals(item.getTransactionType().getValue(), tr.type) //
&& Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)))
.findFirst();
Assertions.assertTrue(optTx.isPresent(), "Required transaction not found: " + tr + " on savings account " + savingsId);
SavingsAccountTransactionData tx = optTx.get();
if (tr.reversed != null) {
Assertions.assertEquals(tr.reversed, tx.getReversed(),
"Transaction is not reversed: " + tr + " on savings account " + savingsId);
}
});
}
}
private RequestSpecification createRequestSpecification(String authKey) {
RequestSpecification requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
requestSpec.header("Authorization", "Basic " + authKey);
requestSpec.header("Fineract-Platform-TenantId", "default");
return requestSpec;
}
protected ResponseSpecification createResponseSpecification(Matcher<Integer> statusCodeMatcher) {
return new ResponseSpecBuilder().expectStatusCode(statusCodeMatcher).build();
}
private String getFullAdminAuthKey() {
return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey();
}
@ToString
@AllArgsConstructor
public static class Transaction {
Double amount;
String type;
String date;
Boolean reversed;
}
public static class InterestPeriodType {
public static final int DAILY = 1;
public static final int MONTHLY = 4;
public static final int QUATERLY = 5;
public static final int BIANNUAL = 6;
}
public static class InterestCalculationType {
public static final int DAILY_BALANCE = 1;
public static final int AVERAGE_DAILY_BALANCE = 2;
}
public static class InterestRecalculationCompoundingMethod {
public static final Integer NONE = 0;
}
public static class DaysInYearType {
public static final Integer INVALID = 0;
public static final Integer ACTUAL = 1;
public static final Integer DAYS_360 = 360;
public static final Integer DAYS_364 = 364;
public static final Integer DAYS_365 = 365;
}
}