Skip to content
Merged
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 @@ -18,6 +18,7 @@
*/
package org.apache.fineract.integrationtests;

import static org.apache.fineract.integrationtests.client.feign.modules.FeignTestConstants.DATETIME_PATTERN;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Expand All @@ -27,37 +28,28 @@

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
import org.apache.fineract.client.models.ChargeRequest;
import org.apache.fineract.client.models.GetSavingsAccountsSavingsAccountIdChargesResponse;
import org.apache.fineract.client.models.GetSavingsAccountsSavingsAccountIdChargesSavingsAccountChargeIdResponse;
import org.apache.fineract.client.models.PostChargesResponse;
import org.apache.fineract.client.models.PostSavingsAccountTransactionsRequest;
import org.apache.fineract.client.models.PostSavingsAccountTransactionsResponse;
import org.apache.fineract.client.models.PostSavingsAccountsSavingsAccountIdChargesRequest;
import org.apache.fineract.client.models.PostSavingsAccountsSavingsAccountIdChargesResponse;
import org.apache.fineract.client.models.PostSavingsProductsRequest;
import org.apache.fineract.client.models.SavingsAccountData;
import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
import org.apache.fineract.integrationtests.savings.base.BaseSavingsIntegrationTest;
import org.apache.fineract.integrationtests.client.feign.FeignSavingsTestBase;
import org.apache.fineract.integrationtests.common.Utils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

@Slf4j
public class SavingsAccountChargeRoundingTest extends BaseSavingsIntegrationTest {
public class SavingsAccountChargeRoundingTest extends FeignSavingsTestBase {

private Long clientId;
private ChargesHelper chargesHelper;
private static final String DATE = "01 January 2026";

@BeforeEach
public void setup() {
clientId = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
chargesHelper = new ChargesHelper();
clientId = createClient();
}

/** FLAT CHARGE **/
Expand All @@ -66,7 +58,7 @@ public void shouldApplyRoundingRules_forFlatCharge() {
runAt(DATE, () -> {

Long productId = createSavingsProduct(0, 1);
Long savingsId = createAndActivateSavingsAccount(productId, DATE);
Long savingsId = createApproveActivateSavings(clientId, productId, DATE);

PostChargesResponse charge = createFlatCharge(19.8);
assertNotNull(charge.getResourceId());
Expand All @@ -86,7 +78,7 @@ public void shouldRoundUpFlatCharge_whenValueIsAboveHalf() {
runAt(DATE, () -> {

Long productId = createSavingsProduct(0, 1);
Long savingsId = createAndActivateSavingsAccount(productId, DATE);
Long savingsId = createApproveActivateSavings(clientId, productId, DATE);

PostChargesResponse charge = createFlatCharge(0.6);
assertNotNull(charge.getResourceId());
Expand All @@ -107,16 +99,16 @@ public void shouldFailToAddFlatCharge_whenAmountRoundsToZero() {
runAt(DATE, () -> {

Long productId = createSavingsProduct(0, 1);
Long savingsId = createAndActivateSavingsAccount(productId, DATE);
Long savingsId = createApproveActivateSavings(clientId, productId, DATE);

PostChargesResponse charge = createFlatCharge(0.4);
assertNotNull(charge.getResourceId());

CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> addFlatCharge(savingsId, charge.getResourceId(), 0.4, DATE));

assertEquals(400, exception.getResponse().code());
assertTrue(exception.getMessage().contains("error.msg.savings.charge.amount.rounded.to.zero"));
assertEquals(400, exception.getStatus());
assertTrue(exception.getResponseBody().contains("error.msg.savings.charge.amount.rounded.to.zero"));
assertNoChargesPersisted(savingsId, charge.getResourceId());
});
}
Expand All @@ -126,17 +118,17 @@ public void shouldApplyRoundingRules_forPercentageOfWithdrawalCharge() {
runAt(DATE, () -> {

Long productId = createSavingsProduct(0, 1);
Long savingsId = createAndActivateSavingsAccount(productId, DATE);
Long savingsId = createApproveActivateSavings(clientId, productId, DATE);

PostChargesResponse charge = createPercentageWithdrawalCharge(2.5);

addPercentageWithdrawalCharge(savingsId, charge.getResourceId(), 2.5);

deposit(savingsId, DATE, BigDecimal.valueOf(1000));
deposit(savingsId, "1000", DATE);

withdraw(savingsId, DATE, BigDecimal.valueOf(615));
withdraw(savingsId, "615", DATE);

SavingsAccountData accountData = getSavingsAccount(savingsId);
SavingsAccountData accountData = getSavingsDetails(savingsId);

BigDecimal actualChargeFees = getActualChargeAmount(accountData);
assertNotNull(actualChargeFees);
Expand All @@ -158,17 +150,17 @@ public void shouldRoundUpPercentageOfWithdrawalCharge_whenValueIsAboveHalf() {
runAt(DATE, () -> {

Long productId = createSavingsProduct(0, 1);
Long savingsId = createAndActivateSavingsAccount(productId, DATE);
Long savingsId = createApproveActivateSavings(clientId, productId, DATE);

PostChargesResponse charge = createPercentageWithdrawalCharge(2.5);

addPercentageWithdrawalCharge(savingsId, charge.getResourceId(), 2.5);

deposit(savingsId, DATE, BigDecimal.valueOf(1000));
deposit(savingsId, "1000", DATE);

withdraw(savingsId, DATE, BigDecimal.valueOf(24));
withdraw(savingsId, "24", DATE);

SavingsAccountData accountData = getSavingsAccount(savingsId);
SavingsAccountData accountData = getSavingsDetails(savingsId);

BigDecimal actualChargeFees = getActualChargeAmount(accountData);
assertNotNull(actualChargeFees);
Expand All @@ -191,17 +183,17 @@ public void shouldIgnorePercentageOfWithdrawalCharge_whenRoundedToZero() {
runAt(DATE, () -> {

Long productId = createSavingsProduct(0, 1);
Long savingsId = createAndActivateSavingsAccount(productId, DATE);
Long savingsId = createApproveActivateSavings(clientId, productId, DATE);

PostChargesResponse charge = createPercentageWithdrawalCharge(2.5);

addPercentageWithdrawalCharge(savingsId, charge.getResourceId(), 2.5);

deposit(savingsId, DATE, BigDecimal.valueOf(1000));
deposit(savingsId, "1000", DATE);

withdraw(savingsId, DATE, BigDecimal.valueOf(20));
withdraw(savingsId, "20", DATE);

SavingsAccountData accountData = getSavingsAccount(savingsId);
SavingsAccountData accountData = getSavingsDetails(savingsId);

BigDecimal actualChargeFees = getActualChargeAmount(accountData);
assertNull(actualChargeFees);
Expand All @@ -218,31 +210,29 @@ public void shouldIgnorePercentageOfWithdrawalCharge_whenRoundedToZero() {
// -----------------------------

private Long createSavingsProduct(int digitsAfterDecimal, int inMultiplesOf) {
return createProduct(baseSavingsProduct(digitsAfterDecimal, inMultiplesOf)).getResourceId();
return createSavingsProduct(baseSavingsProduct(digitsAfterDecimal, inMultiplesOf)).getResourceId();
}

private PostSavingsProductsRequest baseSavingsProduct(int digitsAfterDecimal, int inMultiplesOf) {
return dailyInterestPostingProduct().digitsAfterDecimal(digitsAfterDecimal).inMultiplesOf(inMultiplesOf).currencyCode("USD");
}

private Long createAndActivateSavingsAccount(Long productId, String date) {
Long savingsId = applySavingsAccount(applySavingsRequest(clientId, productId, date)).getSavingsId();
approveSavingsAccount(savingsId, date);
activateSavingsAccount(savingsId, date);
return savingsId;
return new PostSavingsProductsRequest().locale("en").name(Utils.uniqueRandomStringGenerator("DAILY_INTEREST", 6))
.shortName(Utils.uniqueRandomStringGenerator("", 4)).description("Daily interest posting product")
.nominalAnnualInterestRate(10.0).digitsAfterDecimal(digitsAfterDecimal).inMultiplesOf(inMultiplesOf).currencyCode("USD")
.accountingRule(1).interestCalculationDaysInYearType(365).interestCompoundingPeriodType(1).interestCalculationType(2)
.interestPostingPeriodType(1).withdrawalFeeForTransfers(false).enforceMinRequiredBalance(false).allowOverdraft(false)
.withHoldTax(false).isDormancyTrackingActive(false);
}

private PostChargesResponse createFlatCharge(double amount) {
String uniqueChargeName = "Savings Account Flat Charge " + UUID.randomUUID().toString().replace("-", "");
return chargesHelper.createCharges(new ChargeRequest().name(uniqueChargeName).chargeAppliesTo(2) // SAVINGS
return chargesHelper.createCharge(new ChargeRequest().name(uniqueChargeName).chargeAppliesTo(2) // SAVINGS
.chargeTimeType(2) // SPECIFIED DUE DATE
.chargeCalculationType(1) // FLAT
.amount(amount).currencyCode("USD").locale("en").active(true).penalty(false));
}

private PostChargesResponse createPercentageWithdrawalCharge(double percentage) {
String uniqueChargeName = "Savings Account Withdrawal Charge " + UUID.randomUUID().toString().replace("-", "");
return chargesHelper.createCharges(new ChargeRequest().name(uniqueChargeName).chargeAppliesTo(2) // SAVINGS
return chargesHelper.createCharge(new ChargeRequest().name(uniqueChargeName).chargeAppliesTo(2) // SAVINGS
.chargeTimeType(5) // WITHDRAWAL
.chargeCalculationType(2) // % OF AMOUNT
.amount(percentage).currencyCode("USD").locale("en").chargePaymentMode(0).active(true).penalty(false));
Expand All @@ -252,29 +242,14 @@ private Long addFlatCharge(Long savingsId, Long chargeId, double amount, String
PostSavingsAccountsSavingsAccountIdChargesRequest request = new PostSavingsAccountsSavingsAccountIdChargesRequest()
.chargeId(chargeId).amount((float) amount).dateFormat(DATETIME_PATTERN).locale("en").dueDate(date);

PostSavingsAccountsSavingsAccountIdChargesResponse response = ok(
fineractClient().savingsAccountCharges.addSavingsAccountCharge(savingsId, request));

return response.getResourceId();
return addSavingsAccountCharge(savingsId, request).getResourceId();
}

private void addPercentageWithdrawalCharge(Long savingsId, Long chargeId, double amount) {
PostSavingsAccountsSavingsAccountIdChargesRequest request = new PostSavingsAccountsSavingsAccountIdChargesRequest()
.chargeId(chargeId).amount((float) amount).locale("en");

ok(fineractClient().savingsAccountCharges.addSavingsAccountCharge(savingsId, request));
}

private PostSavingsAccountTransactionsResponse withdraw(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.createSavingsAccountTransaction(savingsId, request, "withdrawal"));
}

private GetSavingsAccountsSavingsAccountIdChargesSavingsAccountChargeIdResponse getSavingsAccountCharge(Long savingsId,
Long savingsAccountChargeId) {
return ok(fineractClient().savingsAccountCharges.retrieveSavingsAccountCharge(savingsId, savingsAccountChargeId));
addSavingsAccountCharge(savingsId, request);
}

private BigDecimal applyRoundingRules(BigDecimal amount, int digitsAfterDecimal, int inMultiplesOf) {
Expand Down Expand Up @@ -339,10 +314,6 @@ private BigDecimal calculateExpectedBalance(String deposit, String withdraw, Big
return new BigDecimal(deposit).subtract(new BigDecimal(withdraw)).subtract(expectedChargeFees);
}

private List<GetSavingsAccountsSavingsAccountIdChargesResponse> getSavingsCharges(Long savingsId) {
return ok(fineractClient().savingsAccountCharges.retrieveAllSavingsAccountCharges(savingsId, "all"));
}

private void assertNoChargesPersisted(Long savingsId, Long chargeId) {
var charges = getSavingsCharges(savingsId);
boolean chargeExists = charges.stream().anyMatch(c -> chargeId.equals(c.getChargeId()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,31 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.function.Function;
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.models.DeleteSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.GetSavingsAccountsSavingsAccountIdChargesResponse;
import org.apache.fineract.client.models.GetSavingsAccountsSavingsAccountIdChargesSavingsAccountChargeIdResponse;
import org.apache.fineract.client.models.GetSavingsProductsProductIdResponse;
import org.apache.fineract.client.models.PostSavingsAccountTransactionsResponse;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.PostSavingsAccountsResponse;
import org.apache.fineract.client.models.PostSavingsAccountsSavingsAccountIdChargesRequest;
import org.apache.fineract.client.models.PostSavingsAccountsSavingsAccountIdChargesResponse;
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.SavingsAccountStatusEnumData;
import org.apache.fineract.integrationtests.client.FeignIntegrationTest;
import org.apache.fineract.integrationtests.client.feign.helpers.FeignBusinessDateHelper;
import org.apache.fineract.integrationtests.client.feign.helpers.FeignChargesHelper;
import org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper;
import org.apache.fineract.integrationtests.client.feign.helpers.FeignSavingsHelper;
import org.apache.fineract.integrationtests.client.feign.helpers.FeignSavingsLifecycleExtension;
import org.apache.fineract.integrationtests.client.feign.helpers.FeignSavingsProductHelper;
import org.apache.fineract.integrationtests.client.feign.helpers.FeignSavingsTransactionHelper;
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData;
import org.apache.fineract.integrationtests.common.FineractFeignClientHelper;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.ExtendWith;
Expand All @@ -49,6 +57,8 @@ public abstract class FeignSavingsTestBase extends FeignIntegrationTest {
protected static FeignSavingsTransactionHelper savingsTransactionHelper;
protected static FeignSavingsProductHelper savingsProductHelper;
protected static FeignClientHelper clientHelper;
protected static FeignChargesHelper chargesHelper;
protected static FeignBusinessDateHelper businessDateHelper;

@BeforeAll
public static void setupSavingsHelpers() {
Expand All @@ -57,6 +67,8 @@ public static void setupSavingsHelpers() {
savingsTransactionHelper = new FeignSavingsTransactionHelper(client);
savingsProductHelper = new FeignSavingsProductHelper(client);
clientHelper = new FeignClientHelper(client);
chargesHelper = new FeignChargesHelper(client);
businessDateHelper = new FeignBusinessDateHelper(client);
}

protected Long createClient() {
Expand Down Expand Up @@ -120,4 +132,26 @@ protected void verifySavingsStatus(Long savingsId, Function<SavingsAccountStatus
assertNotNull(savings.getStatus(), "Savings status should not be null");
assertTrue(statusExtractor.apply(savings.getStatus()), "Savings status check failed for account " + savingsId);
}

protected void runAt(String date, Runnable action) {
businessDateHelper.runAt(date, detectDateFormat(date), action);
}

private static String detectDateFormat(String date) {
return date.matches("\\d{4}-\\d{2}-\\d{2}") ? LoanTestData.ISO_DATE_PATTERN : LoanTestData.DATETIME_PATTERN;
}

protected PostSavingsAccountsSavingsAccountIdChargesResponse addSavingsAccountCharge(Long savingsId,
PostSavingsAccountsSavingsAccountIdChargesRequest request) {
return savingsHelper.addSavingsAccountCharge(savingsId, request);
}

protected GetSavingsAccountsSavingsAccountIdChargesSavingsAccountChargeIdResponse getSavingsAccountCharge(Long savingsId,
Long savingsAccountChargeId) {
return savingsHelper.getSavingsAccountCharge(savingsId, savingsAccountChargeId);
}

protected List<GetSavingsAccountsSavingsAccountIdChargesResponse> getSavingsCharges(Long savingsId) {
return savingsHelper.getSavingsCharges(savingsId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@

import static org.apache.fineract.client.feign.util.FeignCalls.ok;

import java.util.List;
import java.util.Map;
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.models.DeleteSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.GetSavingsAccountsSavingsAccountIdChargesResponse;
import org.apache.fineract.client.models.GetSavingsAccountsSavingsAccountIdChargesSavingsAccountChargeIdResponse;
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.PostSavingsAccountsSavingsAccountIdChargesRequest;
import org.apache.fineract.client.models.PostSavingsAccountsSavingsAccountIdChargesResponse;
import org.apache.fineract.client.models.SavingsAccountData;
import org.apache.fineract.client.models.SavingsAccountSummaryData;
import org.apache.fineract.integrationtests.client.feign.modules.SavingsRequestBuilders;
Expand Down Expand Up @@ -94,4 +99,18 @@ public Long createApproveActivateSavings(Long clientId, Long productId, String d
activateSavings(savingsId, date);
return savingsId;
}

public PostSavingsAccountsSavingsAccountIdChargesResponse addSavingsAccountCharge(Long savingsId,
PostSavingsAccountsSavingsAccountIdChargesRequest request) {
return ok(() -> fineractClient.savingsCharges().createSavingsAccountCharge(savingsId, request));
}

public GetSavingsAccountsSavingsAccountIdChargesSavingsAccountChargeIdResponse getSavingsAccountCharge(Long savingsId,
Long savingsAccountChargeId) {
return ok(() -> fineractClient.savingsCharges().retrieveOneSavingsAccountCharge(savingsId, savingsAccountChargeId));
}

public List<GetSavingsAccountsSavingsAccountIdChargesResponse> getSavingsCharges(Long savingsId) {
return ok(() -> fineractClient.savingsCharges().retrieveAllSavingsAccountCharges(savingsId, "all"));
}
}
Loading