diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 79cba8ee1..6fac36276 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -1521,6 +1521,7 @@ public class SavingsAccount extends AbstractPersistableCustom { final List depositAccountOnHoldTransactions, final boolean backdatedTxnsAllowedTill) { List transactionsSortedByDate = null; + BigDecimal transactionAmount = null; if (backdatedTxnsAllowedTill) { transactionsSortedByDate = retrieveSortedTransactions(); } else { @@ -1535,6 +1536,9 @@ public class SavingsAccount extends AbstractPersistableCustom { Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); LocalDate lastSavingsDate = null; for (final SavingsAccountTransaction transaction : transactionsSortedByDate) { + + transactionAmount = transaction.getAmount(); + if (transaction.isNotReversed() && transaction.isCredit()) { runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); } else if (transaction.isNotReversed() && transaction.isDebit()) { @@ -1573,14 +1577,11 @@ public class SavingsAccount extends AbstractPersistableCustom { throw new PlatformApiDataValidationException(dataValidationErrors); } } - } lastSavingsDate = transaction.transactionLocalDate(); - } BigDecimal withdrawalFee = null; - BigDecimal transactionAmount = null; if (isOverdraft()) { if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); @@ -1588,6 +1589,18 @@ public class SavingsAccount extends AbstractPersistableCustom { } } + public void validateAccountBalanceDoesNotViolateOverdraft(final List savingsAccountTransaction, + final BigDecimal amountPaid) { + if (savingsAccountTransaction != null) { + SavingsAccountTransaction savingsAccountTransactionFirst = savingsAccountTransaction.get(0); + if (!this.allowOverdraft) { + if (savingsAccountTransactionFirst.getRunningBalance(this.currency).minus(amountPaid).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), null, amountPaid); + } + } + } + } + protected boolean isAccountLocked(final LocalDate transactionDate) { boolean isLocked = false; final boolean accountHasLockedInSetting = this.lockedInUntilDate != null; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java index 6d2db4080..e0d03b657 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionRepository.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.savings.domain; import java.time.LocalDate; import java.util.List; import javax.persistence.LockModeType; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Lock; @@ -47,6 +48,9 @@ public interface SavingsAccountTransactionRepository @Lock(LockModeType.PESSIMISTIC_WRITE) List findBySavingsAccount(@Param("savingsAccount") SavingsAccount savingsAccount); - @Query("select sat from SavingsAccountTransaction sat where sat.refNo = :refNo") - List findAllTransactionByRefNo(@Param("refNo") String refNo); + List findByRefNo(@Param("refNo") String refNo); + + @Query("select sat from SavingsAccountTransaction sat where sat.savingsAccount.id = :savingsId and sat.dateOf <= :transactionDate and sat.reversed=false") + List findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(@Param("savingsId") Long savingsId, + @Param("transactionDate") LocalDate transactionDate, Pageable pageable); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index fc17fd9bb..89e94e749 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -130,6 +130,9 @@ import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; @@ -718,7 +721,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi List savingsAccountTransactions = null; if (isBulk) { String transactionRefNo = savingsAccountTransaction.getRefNo(); - savingsAccountTransactions = this.savingsAccountTransactionRepository.findAllTransactionByRefNo(transactionRefNo); + savingsAccountTransactions = this.savingsAccountTransactionRepository.findByRefNo(transactionRefNo); reversal = this.savingsAccountDomainService.handleReversal(account, savingsAccountTransactions, backdatedTxnsAllowedTill); } else { reversal = this.savingsAccountDomainService.handleReversal(account, Collections.singletonList(savingsAccountTransaction), @@ -1471,6 +1474,13 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi this.savingAccountAssembler.assignSavingAccountHelpers(account); final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); + Pageable sortedByDateAndIdDesc = PageRequest.of(0, 1, Sort.by("dateOf", "id").descending()); + + List savingsAccountTransaction = this.savingsAccountTransactionRepository + .findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(account.getId(), transactionDate, sortedByDateAndIdDesc); + + account.validateAccountBalanceDoesNotViolateOverdraft(savingsAccountTransaction, amountPaid); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); SavingsAccountTransaction chargeTransaction = account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, user, backdatedTxnsAllowedTill, null); @@ -1488,6 +1498,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); } List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java index 9788a93a9..5e5b65551 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java @@ -2869,6 +2869,64 @@ public class ClientSavingsIntegrationTest { assertEquals(balance.toString(), requestedTransaction.get("runningBalance").toString(), "Equality check for Balance"); } + @Test + public void testSavingsAccountChargesBackDate() { + this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + SavingsAccountHelper savingsAccountHelperValidationError = new SavingsAccountHelper(this.requestSpec, + new ResponseSpecBuilder().build()); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + + final String minBalanceForInterestCalculation = null; + final String minRequiredBalance = null; + final String enforceMinRequiredBalance = "false"; + final boolean allowOverdraft = false; + final Integer savingsProductID = createSavingsProduct(this.requestSpec, this.responseSpec, "0", minBalanceForInterestCalculation, + minRequiredBalance, enforceMinRequiredBalance, allowOverdraft); + Assertions.assertNotNull(savingsProductID); + + final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplication(clientID, savingsProductID, ACCOUNT_TYPE_INDIVIDUAL); + Assertions.assertNotNull(savingsProductID); + + HashMap modifications = this.savingsAccountHelper.updateSavingsAccount(clientID, savingsProductID, savingsId, + ACCOUNT_TYPE_INDIVIDUAL); + Assertions.assertTrue(modifications.containsKey("submittedOnDate")); + + HashMap savingsStatusHashMap = SavingsStatusChecker.getStatusOfSavings(this.requestSpec, this.responseSpec, savingsId); + SavingsStatusChecker.verifySavingsIsPending(savingsStatusHashMap); + + savingsStatusHashMap = this.savingsAccountHelper.approveSavings(savingsId); + SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap); + + savingsStatusHashMap = this.savingsAccountHelper.activateSavings(savingsId); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + + final Integer chargeId = ChargesHelper.createCharges(this.requestSpec, this.responseSpec, + ChargesHelper.getSavingsSpecifiedDueDateJSON()); + Assertions.assertNotNull(chargeId); + + ArrayList charges = this.savingsAccountHelper.getSavingsCharges(savingsId); + Assertions.assertTrue(charges == null || charges.size() == 0); + + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", "05 March 2013", CommonConstants.RESPONSE_RESOURCE_ID); + + this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100", "07 March 2013", CommonConstants.RESPONSE_RESOURCE_ID); + + final Integer savingsChargeId = this.savingsAccountHelper.addChargesForSavingsWithDueDate(savingsId, chargeId, "07 March 2013", + 200); + + ArrayList savingsAccountErrorData = (ArrayList) savingsAccountHelperValidationError + .payChargeToSavingsAccount(savingsId, savingsChargeId, "200", "06 March 2013", CommonConstants.RESPONSE_ERROR); + + assertEquals("error.msg.savingsaccount.transaction.insufficient.account.balance", + savingsAccountErrorData.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); + + final Integer payChargeId = this.savingsAccountHelper.payCharge(savingsChargeId, savingsId, "200", "07 March 2013"); + + Assertions.assertNotNull(payChargeId); + } + @Test public void testRunningBalanceAfterWithdrawalWithBackdateConfigurationOn() { this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java index e76abd6f6..8ce6a5b5b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java @@ -263,6 +263,13 @@ public class SavingsAccountHelper { getSavingsTransactionPaymentTypeJSON(amount, date, paymentType), jsonAttributeToGetback); } + public Object payChargeToSavingsAccount(final Integer savingsID, final Integer chargeId, final String amount, String date, + String jsonAttributeToGetback) { + LOG.info("--------------------------------- PAY SAVINGS CHARGE --------------------------------"); + return performSavingActions(createChargesURL("paycharge", savingsID, chargeId), getSavingsPayChargeJSON(amount, date), + jsonAttributeToGetback); + } + public Integer updateSavingsAccountTransaction(final Integer savingsId, final Integer transactionId, final String amount) { LOG.info("\n--------------------------------- MODIFY SAVINGS TRANSACTION --------------------------------"); return (Integer) performSavingActions(createAdjustTransactionURL(MODIFY_TRASACTION_COMMAND, savingsId, transactionId),