1818 */
1919package org .apache .fineract .integrationtests ;
2020
21+ import static org .apache .fineract .integrationtests .common .BusinessDateHelper .runAt ;
22+
2123import io .restassured .builder .RequestSpecBuilder ;
2224import io .restassured .builder .ResponseSpecBuilder ;
2325import io .restassured .http .ContentType ;
3436import java .util .List ;
3537import java .util .Locale ;
3638import 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 ;
4040import org .apache .fineract .infrastructure .core .service .MathUtil ;
41- import org .apache .fineract .integrationtests .common .BusinessDateHelper ;
4241import org .apache .fineract .integrationtests .common .ClientHelper ;
4342import org .apache .fineract .integrationtests .common .CommonConstants ;
4443import org .apache .fineract .integrationtests .common .GlobalConfigurationHelper ;
5150import org .apache .fineract .integrationtests .common .savings .SavingsProductHelper ;
5251import org .apache .fineract .integrationtests .common .savings .SavingsTestLifecycleExtension ;
5352import org .apache .fineract .portfolio .savings .SavingsAccountTransactionType ;
53+ import org .junit .jupiter .api .AfterEach ;
5454import org .junit .jupiter .api .Assertions ;
5555import org .junit .jupiter .api .BeforeEach ;
5656import 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