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.");
+ }
}