This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 719cba74c2 FINERACT-2312: Post interest with adjustments for savings
accounts
719cba74c2 is described below
commit 719cba74c2d331c0ebf3d1e9a44d2766b3c82393
Author: JohnAlva <[email protected]>
AuthorDate: Fri Sep 5 20:48:52 2025 -0600
FINERACT-2312: Post interest with adjustments for savings accounts
---
.../data/SavingsAccountTransactionData.java | 4 ++
.../SavingsAccountInterestPostingServiceImpl.java | 3 +-
.../SavingsInterestPostingTest.java | 65 ++++++++++++++++------
3 files changed, 55 insertions(+), 17 deletions(-)
diff --git
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java
index 47236df405..76102db832 100644
---
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java
+++
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java
@@ -658,6 +658,10 @@ public final class SavingsAccountTransactionData
implements Serializable {
return
SavingsAccountTransactionType.fromInt(this.transactionType.getId().intValue()).isWithHoldTax()
&& isNotReversed();
}
+ public boolean isAccrual() {
+ return
SavingsAccountTransactionType.fromInt(this.transactionType.getId().intValue()).isAccrual();
+ }
+
public boolean isNotReversed() {
return !isReversed();
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java
index 79831d54f5..19fd9332ad 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java
@@ -535,7 +535,8 @@ public class SavingsAccountInterestPostingServiceImpl
implements SavingsAccountI
}
if (transaction.getId() == null &&
overdraftAmount.isGreaterThanZero()) {
transaction.updateOverdraftAmount(overdraftAmount.getAmount());
- } else if
(overdraftAmount.isNotEqualTo(Money.of(savingsAccountData.getCurrency(),
transaction.getOverdraftAmount()))) {
+ } else if
(overdraftAmount.isNotEqualTo(Money.of(savingsAccountData.getCurrency(),
transaction.getOverdraftAmount()))
+ && !transaction.isAccrual()) {
SavingsAccountTransactionData accountTransaction =
SavingsAccountTransactionData.copyTransaction(transaction);
if (transaction.isChargeTransaction()) {
Set<SavingsAccountChargesPaidByData> chargesPaidBy =
transaction.getSavingsAccountChargesPaid();
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
index 7e03e49487..00331830c6 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
@@ -69,6 +69,9 @@ public class SavingsInterestPostingTest {
private SavingsProductHelper productHelper;
private JournalEntryHelper journalEntryHelper;
+ private static final String ACCRUALS_JOB_NAME = "Add Accrual Transactions
For Savings";
+ private static final String POST_INTEREST_JOB_NAME = "Post Interest For
Savings";
+
@BeforeEach
public void setup() {
Utils.initializeRESTAssured();
@@ -86,7 +89,6 @@ public class SavingsInterestPostingTest {
public void testPostInterestWithOverdraftProduct() {
try {
final String amount = "10000";
- final String jobName = "Post Interest For Savings";
final Account assetAccount = accountHelper.createAssetAccount();
final Account incomeAccount = accountHelper.createIncomeAccount();
@@ -116,7 +118,7 @@ public class SavingsInterestPostingTest {
LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec,
BusinessDateType.BUSINESS_DATE, marchDate);
- schedulerJobHelper.executeAndAwaitJob(jobName);
+ runAccrualsThenPost();
long days = ChronoUnit.DAYS.between(startDate, marchDate);
BigDecimal expected = calcInterestPosting(productHelper, amount,
days);
@@ -130,6 +132,8 @@ public class SavingsInterestPostingTest {
long overdraftCount = countOverdraftOnDate(accountId, marchDate);
Assertions.assertEquals(1L, interestCount, "Expected exactly one
INTEREST posting on posting date");
Assertions.assertEquals(0L, overdraftCount, "Expected NO OVERDRAFT
posting on posting date");
+
+ assertNoAccrualReversals(accountId);
} finally {
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(false));
@@ -140,7 +144,6 @@ public class SavingsInterestPostingTest {
public void testOverdraftInterestWithOverdraftProduct() {
try {
final String amount = "10000";
- final String jobName = "Post Interest For Savings";
final Account assetAccount = accountHelper.createAssetAccount();
final Account incomeAccount = accountHelper.createIncomeAccount();
@@ -170,7 +173,7 @@ public class SavingsInterestPostingTest {
LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec,
BusinessDateType.BUSINESS_DATE, marchDate);
- schedulerJobHelper.executeAndAwaitJob(jobName);
+ runAccrualsThenPost();
long days = ChronoUnit.DAYS.between(startDate, marchDate);
BigDecimal expected = calcOverdraftPosting(productHelper, amount,
days);
@@ -185,6 +188,8 @@ public class SavingsInterestPostingTest {
long overdraftCount = countOverdraftOnDate(accountId, marchDate);
Assertions.assertEquals(0L, interestCount, "Expected NO INTEREST
posting on posting date");
Assertions.assertEquals(1L, overdraftCount, "Expected exactly one
OVERDRAFT posting on posting date");
+
+ assertNoAccrualReversals(accountId);
} finally {
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(false));
@@ -196,7 +201,6 @@ public class SavingsInterestPostingTest {
try {
final String amountDeposit = "10000";
final String amountWithdrawal = "20000";
- final String jobName = "Post Interest For Savings";
final Account assetAccount = accountHelper.createAssetAccount();
final Account incomeAccount = accountHelper.createIncomeAccount();
@@ -230,7 +234,7 @@ public class SavingsInterestPostingTest {
LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec,
BusinessDateType.BUSINESS_DATE, marchDate);
- schedulerJobHelper.executeAndAwaitJob(jobName);
+ runAccrualsThenPost();
List<HashMap> txs = getInterestTransactions(accountId);
for (HashMap tx : txs) {
@@ -254,6 +258,8 @@ public class SavingsInterestPostingTest {
Assertions.assertEquals(1L, countInterestOnDate(accountId,
marchDate), "Expected exactly one INTEREST posting on posting date");
Assertions.assertEquals(1L, countOverdraftOnDate(accountId,
marchDate),
"Expected exactly one OVERDRAFT posting on posting date");
+
+ assertNoAccrualReversals(accountId);
} finally {
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(false));
@@ -265,7 +271,6 @@ public class SavingsInterestPostingTest {
try {
final String amountDeposit = "20000";
final String amountWithdrawal = "10000";
- final String jobName = "Post Interest For Savings";
final Account assetAccount = accountHelper.createAssetAccount();
final Account incomeAccount = accountHelper.createIncomeAccount();
@@ -298,7 +303,7 @@ public class SavingsInterestPostingTest {
LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec,
BusinessDateType.BUSINESS_DATE, marchDate);
- schedulerJobHelper.executeAndAwaitJob(jobName);
+ runAccrualsThenPost();
List<HashMap> txs = getInterestTransactions(accountId);
for (HashMap tx : txs) {
@@ -322,6 +327,8 @@ public class SavingsInterestPostingTest {
Assertions.assertEquals(1L, countOverdraftOnDate(accountId,
marchDate),
"Expected exactly one OVERDRAFT posting on posting date");
Assertions.assertEquals(1L, countInterestOnDate(accountId,
marchDate), "Expected exactly one INTEREST posting on posting date");
+
+ assertNoAccrualReversals(accountId);
} finally {
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(false));
@@ -374,13 +381,8 @@ public class SavingsInterestPostingTest {
return principal.multiply(periodRate,
MathContext.DECIMAL64).setScale(productHelper.getDecimalCurrency(),
RoundingMode.HALF_EVEN);
}
- // =========================
- // Helpers robustos de FECHA/TIPO
- // =========================
-
@SuppressWarnings("unchecked")
private LocalDate coerceToLocalDate(HashMap tx) {
- // Posibles claves de fecha devueltas por Fineract
String[] candidateKeys = new String[] { "date", "transactionDate",
"submittedOnDate", "createdDate" };
for (String key : candidateKeys) {
@@ -389,7 +391,6 @@ public class SavingsInterestPostingTest {
continue;
}
- // Caso 1: arreglo [yyyy, MM, dd]
if (v instanceof List<?>) {
List<?> arr = (List<?>) v;
if (arr.size() >= 3 && arr.get(0) instanceof Number &&
arr.get(1) instanceof Number && arr.get(2) instanceof Number) {
@@ -400,7 +401,6 @@ public class SavingsInterestPostingTest {
}
}
- // Caso 2: string con distintos formatos
if (v instanceof String) {
String s = (String) v;
DateTimeFormatter[] fmts = new DateTimeFormatter[] {
DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US),
@@ -414,7 +414,6 @@ public class SavingsInterestPostingTest {
}
}
}
- // Si ninguna clave/forma se pudo parsear
return null;
}
@@ -440,4 +439,38 @@ public class SavingsInterestPostingTest {
return all.stream().filter(tx -> isDate(tx, date)).map(this::txType)
.filter(SavingsAccountTransactionType::isOverDraftInterestPosting).count();
}
+
+ private void runAccrualsThenPost() {
+ try {
+ schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
+ } catch (IllegalArgumentException ex) {
+ LOG.warn("Accruals job not found ({}). Continuing without it.",
ACCRUALS_JOB_NAME, ex);
+ }
+ schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
+ }
+
+ @SuppressWarnings({ "rawtypes" })
+ private boolean isReversed(HashMap tx) {
+ Object v = tx.get("reversed");
+ if (v instanceof Boolean) {
+ return (Boolean) v;
+ }
+ if (v instanceof Number) {
+ return ((Number) v).intValue() != 0;
+ }
+ if (v instanceof String) {
+ return Boolean.parseBoolean((String) v);
+ }
+ return false;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void assertNoAccrualReversals(Integer accountId) {
+ List<HashMap> all =
savingsAccountHelper.getSavingsTransactions(accountId);
+ long reversedAccruals = all.stream().filter(tx -> {
+ SavingsAccountTransactionType t = txType(tx);
+ return t.isAccrual() && isReversed(tx);
+ }).count();
+ Assertions.assertEquals(0L, reversedAccruals, "Accrual reversals were
found in account transactions");
+ }
}