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 fb8f2f7cb6 FINERACT-2312: Reverse of Accruals to adjust late deposits
fb8f2f7cb6 is described below

commit fb8f2f7cb6fb8a557fb2a55daff7b81bd308bc61
Author: JohnAlva <[email protected]>
AuthorDate: Tue Jul 29 11:20:07 2025 -0600

    FINERACT-2312: Reverse of Accruals to adjust late deposits
---
 .../SavingsAccrualWritePlatformServiceImpl.java    |  8 ++-
 .../portfolio/savings/domain/SavingsAccount.java   | 23 ++++++-
 .../SavingsAccrualIntegrationTest.java             | 74 ++++++++++++++++++++++
 3 files changed, 102 insertions(+), 3 deletions(-)

diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java
index 6fc136b119..1d61eaa9f9 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java
@@ -158,13 +158,19 @@ public class SavingsAccrualWritePlatformServiceImpl 
implements SavingsAccrualWri
 
         final List<LocalDate> accrualTransactionDates = 
savingsAccount.retrieveOrderedAccrualTransactions().stream()
                 .map(transaction -> transaction.getTransactionDate()).toList();
+        final List<LocalDate> reversedAccrualTransactionDates = 
savingsAccount.retrieveOrderedAccrualTransactions().stream()
+                .filter(transaction -> 
transaction.isReversed()).map(transaction -> 
transaction.getTransactionDate()).toList();
+
         LocalDate accruedTillDate = fromDate;
 
         for (PostingPeriod period : allPostingPeriods) {
+            LocalDate valueDate = period.getPeriodInterval().endDate();
+            List<LocalDate> matchingAccrualDates = 
reversedAccrualTransactionDates.stream()
+                    .filter(accrualDate -> 
accrualDate.equals(valueDate)).toList();
             period.calculateInterest(compoundInterestValues);
             final LocalDate endDate = period.getPeriodInterval().endDate();
             if 
(!accrualTransactionDates.contains(period.getPeriodInterval().endDate())
-                    && !MathUtil.isZero(period.closingBalance().getAmount())) {
+                    && (!MathUtil.isZero(period.closingBalance().getAmount()) 
|| !matchingAccrualDates.isEmpty())) {
                 String refNo = (refNoProvider != null) ? 
refNoProvider.apply(endDate) : null;
                 SavingsAccountTransaction savingsAccountTransaction = 
SavingsAccountTransaction.accrual(savingsAccount,
                         savingsAccount.office(), 
period.getPeriodInterval().endDate(), period.getInterestEarned().abs(), false, 
refNo);
diff --git 
a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
 
b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
index e2f226eb46..50b2db3987 100644
--- 
a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
+++ 
b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
@@ -1044,7 +1044,8 @@ public class SavingsAccount extends 
AbstractAuditableWithUTCDateTimeCustom<Long>
                 }
                 if (!calculateInterest || transaction.getId() == null) {
                     transaction.setOverdraftAmount(overdraftAmount);
-                } else if (!MathUtil.isEqualTo(overdraftAmount, 
transaction.getOverdraftAmount(this.currency))) {
+                } else if (!MathUtil.isEqualTo(overdraftAmount, 
transaction.getOverdraftAmount(this.currency))
+                        && !transaction.isAccrual()) {
                     SavingsAccountTransaction accountTransaction = 
SavingsAccountTransaction.copyTransaction(transaction);
                     if (transaction.isChargeTransaction()) {
                         Set<SavingsAccountChargePaidBy> chargesPaidBy = 
transaction.getSavingsAccountChargesPaid();
@@ -1171,6 +1172,7 @@ public class SavingsAccount extends 
AbstractAuditableWithUTCDateTimeCustom<Long>
         if (backdatedTxnsAllowedTill) {
             addTransactionToExisting(transaction);
         } else {
+            this.accrualsForSavingsReverse(transactionDTO, 
backdatedTxnsAllowedTill);
             addTransaction(transaction);
         }
 
@@ -1306,6 +1308,7 @@ public class SavingsAccount extends 
AbstractAuditableWithUTCDateTimeCustom<Long>
         if (backdatedTxnsAllowedTill) {
             addTransactionToExisting(transaction);
         } else {
+            this.accrualsForSavingsReverse(transactionDTO, 
backdatedTxnsAllowedTill);
             addTransaction(transaction);
         }
 
@@ -3865,10 +3868,26 @@ public class SavingsAccount extends 
AbstractAuditableWithUTCDateTimeCustom<Long>
                 .toList();
     }
 
+    public void accrualsForSavingsReverse(SavingsAccountTransactionDTO 
transactionDTO, final boolean backdatedTxnsAllowedTill) {
+        List<SavingsAccountTransaction> accountTransactionsSorted = null;
+
+        if (backdatedTxnsAllowedTill) {
+            accountTransactionsSorted = retrieveSortedTransactions();
+        } else {
+            accountTransactionsSorted = retrieveListOfTransactions();
+        }
+        for (final SavingsAccountTransaction transaction : 
accountTransactionsSorted) {
+            boolean typeTransaccionValidation = 
transaction.getTransactionType() == SavingsAccountTransactionType.ACCRUAL;
+            if (typeTransaccionValidation && 
(transaction.getDateOf().isAfter(transactionDTO.getTransactionDate())
+                    || 
transaction.getDateOf().isEqual(transactionDTO.getTransactionDate()))) {
+                transaction.reverse();
+            }
+        }
+    }
+
     public List<SavingsAccountTransactionDetailsForPostingPeriod> 
toSavingsAccountTransactionDetailsForPostingPeriodList() {
         return retreiveOrderedNonInterestPostingTransactions().stream()
                 .map(transaction -> 
transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, 
this.allowOverdraft))
                 .toList();
     }
-
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccrualIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccrualIntegrationTest.java
index af4ab8ba01..c076ee8227 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccrualIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccrualIntegrationTest.java
@@ -148,4 +148,78 @@ public class SavingsAccrualIntegrationTest {
                     "The total accrual (" + actualTotalAccrual + ") does not 
match the expected (" + expectedTotalAccrual + ")");
         });
     }
+
+    @Test
+    public void testAccrualsAreReversedImmediatelyAfterBackdatedTransaction() {
+        // --- ARRANGE ---
+        final Account assetAccount = this.accountHelper.createAssetAccount();
+        final Account liabilityAccount = 
this.accountHelper.createLiabilityAccount();
+        final Account incomeAccount = this.accountHelper.createIncomeAccount();
+        final Account expenseAccount = 
this.accountHelper.createExpenseAccount();
+        final String interestRate = "10.0";
+        final int daysToTest = 10;
+        final int daysUntilTransaction = 5;
+
+        final SavingsProductHelper productHelper = new 
SavingsProductHelper().withInterestCompoundingPeriodTypeAsDaily()
+                
.withInterestPostingPeriodTypeAsMonthly().withInterestCalculationPeriodTypeAsDailyBalance()
+                .withNominalAnnualInterestRate(new BigDecimal(interestRate))
+                .withAccountingRuleAsAccrualBased(new Account[] { 
assetAccount, liabilityAccount, incomeAccount, expenseAccount });
+
+        final Integer savingsProductId = 
SavingsProductHelper.createSavingsProduct(productHelper.build(), 
this.requestSpec,
+                this.responseSpec);
+        Assertions.assertNotNull(savingsProductId);
+
+        final Integer clientId = ClientHelper.createClient(this.requestSpec, 
this.responseSpec, "01 January 2020");
+        Assertions.assertNotNull(clientId);
+
+        final LocalDate startDate = 
LocalDate.now(Utils.getZoneIdOfTenant()).minusDays(daysToTest);
+        final String startDateString = DateTimeFormatter.ofPattern("dd MMMM 
yyyy", Locale.US).format(startDate);
+
+        final Integer savingsAccountId = 
this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, 
savingsProductId,
+                SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, startDateString);
+        Assertions.assertNotNull(savingsAccountId);
+
+        this.savingsAccountHelper.approveSavingsOnDate(savingsAccountId, 
startDateString);
+        this.savingsAccountHelper.activateSavings(savingsAccountId, 
startDateString);
+
+        this.savingsAccountHelper.depositToSavingsAccount(savingsAccountId, 
"10000", startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
+
+        schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions For 
Savings");
+        LOG.info("Initial accruals for {} days have been generated.", 
daysToTest);
+
+        // --- ACT ---
+        final LocalDate backdatedTransactionDate = 
startDate.plusDays(daysUntilTransaction);
+        final String backdatedTransactionDateString = 
DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US)
+                .format(backdatedTransactionDate);
+
+        LOG.info("Performing a single backdated withdrawal on {}. Expecting 
immediate reversal of subsequent accruals.",
+                backdatedTransactionDateString);
+        
this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsAccountId, 
"1000", backdatedTransactionDateString,
+                CommonConstants.RESPONSE_RESOURCE_ID);
+
+        // --- ASSERT ---
+        List<HashMap> allTransactions = 
savingsAccountHelper.getSavingsTransactions(savingsAccountId);
+
+        boolean verifiedReversedTransaction = false;
+
+        for (HashMap<String, Object> transaction : allTransactions) {
+            Map<String, Object> type = (Map<String, Object>) 
transaction.get("transactionType");
+            if (type != null && Boolean.TRUE.equals(type.get("accrual"))) {
+                List<Number> dateArray = (List<Number>) 
transaction.get("date");
+                LocalDate transactionDate = 
LocalDate.of(dateArray.get(0).intValue(), dateArray.get(1).intValue(),
+                        dateArray.get(2).intValue());
+
+                if (!transactionDate.isBefore(backdatedTransactionDate)) {
+                    
Assertions.assertTrue(Boolean.TRUE.equals(transaction.get("reversed")), 
"Accrual on or after "
+                            + backdatedTransactionDate + " should be REVERSED. 
Transaction date: " + transactionDate);
+                    verifiedReversedTransaction = true;
+                } else {
+                    
Assertions.assertFalse(Boolean.TRUE.equals(transaction.get("reversed")),
+                            "Accrual before " + backdatedTransactionDate + " 
should NOT be reversed. Transaction date: " + transactionDate);
+                }
+            }
+        }
+
+        Assertions.assertTrue(verifiedReversedTransaction, "Test failed 
because no reversed accrual transactions were found to verify.");
+    }
 }

Reply via email to