Skip to content

Commit 5c8d86c

Browse files
Tedyyy-Alburadamsaghy
authored andcommitted
FINERACT-2394:Fixed Deposits is creating double Interest Postings
1 parent 8c3c44b commit 5c8d86c

File tree

2 files changed

+108
-81
lines changed

2 files changed

+108
-81
lines changed

fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/postinterestforsavings/PostInterestForSavingTasklet.java

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -166,22 +166,6 @@ private void postInterest(List<SavingsAccountData> savingsAccounts, int threadPo
166166

167167
List<Future<Void>> responses = new ArrayList<>();
168168
posters.forEach(poster -> responses.add(taskExecutor.submit(poster)));
169-
Long maxId = maxSavingsIdInList;
170-
if (!queue.isEmpty()) {
171-
maxId = Math.max(maxSavingsIdInList, queue.element().get(queue.element().size() - 1).getId());
172-
}
173-
174-
while (queue.size() <= QUEUE_SIZE) {
175-
log.debug("Fetching while threads are running!..:: this is not supposed to run........");
176-
savingsAccounts = Collections.synchronizedList(this.savingAccountReadPlatformService
177-
.retrieveAllSavingsDataForInterestPosting(backdatedTxnsAllowedTill, pageSize, ACTIVE.getValue(), maxId));
178-
if (savingsAccounts.isEmpty()) {
179-
break;
180-
}
181-
maxId = savingsAccounts.get(savingsAccounts.size() - 1).getId();
182-
log.debug("Add to the Queue");
183-
queue.add(savingsAccounts);
184-
}
185169

186170
checkCompletion(responses);
187171
log.debug("Queue size {}", queue.size());

integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java

Lines changed: 108 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.apache.fineract.integrationtests;
2020

21+
import static org.apache.fineract.integrationtests.common.BusinessDateHelper.runAt;
22+
2123
import io.restassured.builder.RequestSpecBuilder;
2224
import io.restassured.builder.ResponseSpecBuilder;
2325
import io.restassured.http.ContentType;
@@ -34,11 +36,8 @@
3436
import java.util.List;
3537
import java.util.Locale;
3638
import java.util.Map;
37-
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
38-
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
39-
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
39+
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdRequest;
4040
import org.apache.fineract.infrastructure.core.service.MathUtil;
41-
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
4241
import org.apache.fineract.integrationtests.common.ClientHelper;
4342
import org.apache.fineract.integrationtests.common.CommonConstants;
4443
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
@@ -51,6 +50,7 @@
5150
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
5251
import org.apache.fineract.integrationtests.common.savings.SavingsTestLifecycleExtension;
5352
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
53+
import org.junit.jupiter.api.AfterEach;
5454
import org.junit.jupiter.api.Assertions;
5555
import org.junit.jupiter.api.BeforeEach;
5656
import org.junit.jupiter.api.Test;
@@ -88,9 +88,14 @@ public void setup() {
8888
this.globalConfigurationHelper = new GlobalConfigurationHelper();
8989
}
9090

91+
@AfterEach
92+
public void cleanupAfterTest() {
93+
cleanupSavingsAccountsFromDuplicatePreventionTest();
94+
}
95+
9196
@Test
9297
public void testPostInterestWithOverdraftProduct() {
93-
try {
98+
runAt("12 March 2025", () -> {
9499
final String amount = "10000";
95100

96101
final Account assetAccount = accountHelper.createAssetAccount();
@@ -115,13 +120,10 @@ public void testPostInterestWithOverdraftProduct() {
115120
savingsAccountHelper.activateSavings(accountId, startDateString);
116121
savingsAccountHelper.depositToSavingsAccount(accountId, amount, startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
117122

118-
// Simulate time passing - update business date to March
119-
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
120-
new PutGlobalConfigurationsRequest().enabled(true));
121123
LocalDate marchDate = LocalDate.of(2025, 3, 2);
122-
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, marchDate);
123124

124-
runAccrualsThenPost();
125+
schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
126+
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
125127

126128
long days = ChronoUnit.DAYS.between(startDate, marchDate.minusDays(1));
127129
BigDecimal expected = calcInterestPosting(productHelper, amount, days);
@@ -135,15 +137,12 @@ public void testPostInterestWithOverdraftProduct() {
135137
Assertions.assertEquals(0L, overdraftCount, "Expected NO OVERDRAFT posting on posting date");
136138

137139
assertNoAccrualReversals(accountId);
138-
} finally {
139-
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
140-
new PutGlobalConfigurationsRequest().enabled(false));
141-
}
140+
});
142141
}
143142

144143
@Test
145144
public void testOverdraftInterestWithOverdraftProduct() {
146-
try {
145+
runAt("12 March 2025", () -> {
147146
final String amount = "10000";
148147

149148
final Account assetAccount = accountHelper.createAssetAccount();
@@ -168,13 +167,10 @@ public void testOverdraftInterestWithOverdraftProduct() {
168167
savingsAccountHelper.activateSavings(accountId, startDateString);
169168
savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amount, startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
170169

171-
// Simulate time passing - update business date to March
172-
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
173-
new PutGlobalConfigurationsRequest().enabled(true));
174170
LocalDate marchDate = LocalDate.of(2025, 3, 2);
175-
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, marchDate);
176171

177-
runAccrualsThenPost();
172+
schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
173+
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
178174

179175
long days = ChronoUnit.DAYS.between(startDate, marchDate.minusDays(1));
180176
BigDecimal expected = calcOverdraftPosting(productHelper, amount, days);
@@ -191,15 +187,12 @@ public void testOverdraftInterestWithOverdraftProduct() {
191187
Assertions.assertEquals(1L, overdraftCount, "Expected exactly one OVERDRAFT posting on posting date");
192188

193189
assertNoAccrualReversals(accountId);
194-
} finally {
195-
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
196-
new PutGlobalConfigurationsRequest().enabled(false));
197-
}
190+
});
198191
}
199192

200193
@Test
201194
public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceLessZero() {
202-
try {
195+
runAt("12 March 2025", () -> {
203196
final String amountDeposit = "10000";
204197
final String amountWithdrawal = "20000";
205198

@@ -230,12 +223,10 @@ public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceLess
230223
savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amountWithdrawal, withdrawalStr,
231224
CommonConstants.RESPONSE_RESOURCE_ID);
232225

233-
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
234-
new PutGlobalConfigurationsRequest().enabled(true));
235226
LocalDate marchDate = LocalDate.of(2025, 3, 2);
236-
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, marchDate);
237227

238-
runAccrualsThenPost();
228+
schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
229+
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
239230

240231
List<HashMap> txs = getInterestTransactions(accountId);
241232
for (HashMap tx : txs) {
@@ -262,15 +253,12 @@ public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceLess
262253
"Expected exactly one OVERDRAFT posting on posting date");
263254

264255
assertNoAccrualReversals(accountId);
265-
} finally {
266-
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
267-
new PutGlobalConfigurationsRequest().enabled(false));
268-
}
256+
});
269257
}
270258

271259
@Test
272260
public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceGreaterZero() {
273-
try {
261+
runAt("12 March 2025", () -> {
274262
final String amountDeposit = "20000";
275263
final String amountWithdrawal = "10000";
276264

@@ -300,12 +288,10 @@ public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceGrea
300288
final String depositStr = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(depositDate);
301289
savingsAccountHelper.depositToSavingsAccount(accountId, amountDeposit, depositStr, CommonConstants.RESPONSE_RESOURCE_ID);
302290

303-
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
304-
new PutGlobalConfigurationsRequest().enabled(true));
305291
LocalDate marchDate = LocalDate.of(2025, 3, 2);
306-
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, marchDate);
307292

308-
runAccrualsThenPost();
293+
schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
294+
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
309295

310296
List<HashMap> txs = getInterestTransactions(accountId);
311297
for (HashMap tx : txs) {
@@ -332,15 +318,12 @@ public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceGrea
332318
"Expected exactly one INTEREST posting on posting date");
333319

334320
assertNoAccrualReversals(accountId);
335-
} finally {
336-
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
337-
new PutGlobalConfigurationsRequest().enabled(false));
338-
}
321+
});
339322
}
340323

341324
@Test
342325
public void testPostInterestNotZero() {
343-
try {
326+
runAt("12 March 2025", () -> {
344327
final String amountDeposit = "1000";
345328
final String amountWithdrawal = "1000";
346329

@@ -366,20 +349,17 @@ public void testPostInterestNotZero() {
366349
savingsAccountHelper.activateSavings(accountId, startStr);
367350
savingsAccountHelper.depositToSavingsAccount(accountId, amountDeposit, startStr, CommonConstants.RESPONSE_RESOURCE_ID);
368351

369-
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
370-
new PutGlobalConfigurationsRequest().enabled(true));
371-
LocalDate februaryDate = LocalDate.of(startDate.getYear(), 2, 1);
372-
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, februaryDate);
352+
LocalDate februaryDate = LocalDate.of(2025, 2, 1);
373353

374354
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
375355

376-
List<HashMap> txsFebruary = getInterestTransactions(accountId); // OBTENER EL POSTEO DEL INTEREST
356+
List<HashMap> txsFebruary = getInterestTransactions(accountId);
377357

378358
long daysFebruary = ChronoUnit.DAYS.between(startDate, februaryDate);
379359
BigDecimal expectedFebruary = calcInterestPosting(productHelper, amountDeposit, daysFebruary);
380360
Assertions.assertEquals(expectedFebruary, BigDecimal.valueOf(((Double) txsFebruary.get(0).get("amount"))));
381361

382-
final LocalDate withdrawalDate = LocalDate.of(startDate.getYear(), 2, 1);
362+
final LocalDate withdrawalDate = LocalDate.of(2025, 2, 1);
383363
final String withdrawal = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(withdrawalDate);
384364

385365
BigDecimal runningBalance = new BigDecimal(txsFebruary.get(0).get("runningBalance").toString());
@@ -390,13 +370,12 @@ public void testPostInterestNotZero() {
390370
savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amountWithdrawal, withdrawal,
391371
CommonConstants.RESPONSE_RESOURCE_ID);
392372

393-
LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1);
394-
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, marchDate);
373+
LocalDate marchDate = LocalDate.of(2025, 3, 1);
395374

396375
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
397376

398-
List<HashMap> txs = getInterestTransactions(accountId); // CON ESTE DEBEMOS DE VALIDAR QUE EL DIA DE MARZO
399-
// NO SE TENGA POSTEO EN CERO
377+
List<HashMap> txs = getInterestTransactions(accountId);
378+
400379
for (HashMap tx : txs) {
401380
BigDecimal amt = BigDecimal.valueOf(((Double) tx.get("amount")));
402381
@SuppressWarnings("unchecked")
@@ -418,9 +397,82 @@ public void testPostInterestNotZero() {
418397
"Expected exactly one OVERDRAFT posting on posting date");
419398

420399
assertNoAccrualReversals(accountId);
421-
} finally {
422-
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
423-
new PutGlobalConfigurationsRequest().enabled(false));
400+
});
401+
}
402+
403+
@Test
404+
public void testPostInterestForDuplicatePrevention() {
405+
runAt("18 March 2025", () -> {
406+
final String amount = "10000";
407+
408+
final Account assetAccount = accountHelper.createAssetAccount();
409+
final Account incomeAccount = accountHelper.createIncomeAccount();
410+
final Account expenseAccount = accountHelper.createExpenseAccount();
411+
final Account liabilityAccount = accountHelper.createLiabilityAccount();
412+
final Account interestReceivableAccount = accountHelper.createAssetAccount("interestReceivableAccount");
413+
final Account savingsControlAccount = accountHelper.createLiabilityAccount("Savings Control");
414+
final Account interestPayableAccount = accountHelper.createLiabilityAccount("Interest Payable");
415+
416+
final Integer productId = createSavingsProductWithAccrualAccountingWithOutOverdraftAllowed(
417+
interestPayableAccount.getAccountID().toString(), savingsControlAccount.getAccountID().toString(),
418+
interestReceivableAccount.getAccountID().toString(), assetAccount, incomeAccount, expenseAccount, liabilityAccount);
419+
420+
final LocalDate startDate = LocalDate.of(2025, 2, 1);
421+
422+
List<Integer> accountIdList = new ArrayList<>();
423+
for (int i = 0; i < 800; i++) {
424+
425+
final Integer clientId = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2025");
426+
final String startDateString = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(startDate);
427+
final Integer accountId = savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, productId,
428+
SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, startDateString);
429+
430+
savingsAccountHelper.approveSavingsOnDate(accountId, startDateString);
431+
savingsAccountHelper.activateSavings(accountId, startDateString);
432+
savingsAccountHelper.depositToSavingsAccount(accountId, amount, startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
433+
434+
accountIdList.add(accountId);
435+
}
436+
Assertions.assertEquals(800, accountIdList.size(), "ERROR: Expected 800");
437+
438+
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
439+
440+
for (Integer accountId : accountIdList) {
441+
List<HashMap> txs = getInterestTransactions(accountId);
442+
Assertions.assertEquals(1, txs.size(), "ERROR: Duplicate interest postings exist.");
443+
}
444+
});
445+
}
446+
447+
private void cleanupSavingsAccountsFromDuplicatePreventionTest() {
448+
try {
449+
LOG.info("Starting cleanup of savings accounts after duplicate prevention test");
450+
451+
List<Long> savingsIds = SavingsAccountHelper.getSavingsIdsByStatusId(300);
452+
if (!savingsIds.isEmpty()) {
453+
LOG.info("Found {} savings accounts to cleanup", savingsIds.size());
454+
455+
savingsIds.forEach(savingsId -> {
456+
try {
457+
458+
savingsAccountHelper.postInterestForSavings(savingsId.intValue());
459+
460+
savingsAccountHelper.closeSavingsAccount(savingsId,
461+
new PostSavingsAccountsAccountIdRequest().locale("en").dateFormat(Utils.DATE_FORMAT)
462+
.closedOnDate(Utils.dateFormatter.format(Utils.getLocalDateOfTenant())).withdrawBalance(true));
463+
464+
LOG.debug("Savings account {} closed successfully", savingsId);
465+
} catch (Exception e) {
466+
LOG.warn("Unable to close savings account {}: {}", savingsId, e.getMessage());
467+
}
468+
});
469+
470+
LOG.info("Savings accounts cleanup completed");
471+
} else {
472+
LOG.info("No savings accounts found to cleanup");
473+
}
474+
} catch (Exception e) {
475+
LOG.error("Error during savings accounts cleanup: {}", e.getMessage(), e);
424476
}
425477
}
426478

@@ -528,15 +580,6 @@ private long countOverdraftOnDate(Integer accountId, LocalDate date) {
528580
.filter(SavingsAccountTransactionType::isOverDraftInterestPosting).count();
529581
}
530582

531-
private void runAccrualsThenPost() {
532-
try {
533-
schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
534-
} catch (IllegalArgumentException ex) {
535-
LOG.warn("Accruals job not found ({}). Continuing without it.", ACCRUALS_JOB_NAME, ex);
536-
}
537-
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
538-
}
539-
540583
@SuppressWarnings({ "rawtypes" })
541584
private boolean isReversed(HashMap tx) {
542585
Object v = tx.get("reversed");

0 commit comments

Comments
 (0)