diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java index 85a6547ea..8a2a7268e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -104,4 +104,10 @@ public interface ConfigurationDomainService { String getAccountMappingForCharge(); boolean isNextDayFixedDepositInterestTransferEnabledForPeriodEnd(); + + boolean retrievePivotDateConfig(); + + boolean isRelaxingDaysConfigForPivotDateEnabled(); + + Long retrieveRelaxingDaysConfigForPivotDate(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 4db4d938a..51f76b7e8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -343,6 +343,31 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService return value; } + @Override + public boolean retrievePivotDateConfig() { + final String propertyName = "allow-backdated-transaction-before-interest-posting"; + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName); + return !property.isEnabled(); + + } + + @Override + public boolean isRelaxingDaysConfigForPivotDateEnabled() { + final String propertyName = "allow-backdated-transaction-before-interest-posting-date-for-days"; + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName); + return property.isEnabled(); + } + + @Override + public Long retrieveRelaxingDaysConfigForPivotDate() { + final String propertyName = "allow-backdated-transaction-before-interest-posting-date-for-days"; + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName); + if (property.getValue() == null) { + return Long.valueOf(0); + } + return property.getValue(); + } + private GlobalConfigurationPropertyData getGlobalConfigurationPropertyData(final String propertyName) { String identifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier(); String key = identifier + "_" + propertyName; diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java index b5c4e53c5..4a1ea511b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java @@ -427,6 +427,7 @@ public class InteropServiceImpl implements InteropService { LocalDate transactionDate = transactionDateTime.toLocalDate(); DateTimeFormatter fmt = getDateTimeFormatter(command); SavingsAccountTransaction transaction; + final boolean backdatedTxnsAllowedTill = false; if (isDebit) { SavingsAccountTransaction holdTransaction = findTransaction(savingsAccount, transferCode, AMOUNT_HOLD.getValue()); @@ -457,10 +458,11 @@ public class InteropServiceImpl implements InteropService { SavingsTransactionBooleanValues transactionValues = new SavingsTransactionBooleanValues(false, true, true, false, false); transaction = savingsAccountService.handleWithdrawal(savingsAccount, fmt, transactionDate, request.getAmount().getAmount(), instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, null), - transactionValues); + transactionValues, backdatedTxnsAllowedTill); } else { transaction = savingsAccountService.handleDeposit(savingsAccount, fmt, transactionDate, request.getAmount().getAmount(), - instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, null), false, true); + instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, null), false, true, + backdatedTxnsAllowedTill); } String note = request.getNote(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetailAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetailAssembler.java index 0ba93136e..a8fe76f2a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetailAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetailAssembler.java @@ -64,10 +64,11 @@ public class AccountTransferDetailAssembler { public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) { final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName); - final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId); + final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false); + final boolean backdatedTxnsAllowedTill = false; final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName); - final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill); return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount); @@ -76,7 +77,8 @@ public class AccountTransferDetailAssembler { public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) { final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); - final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId); + final boolean backdatedTxnsAllowedTill = false; + final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill); final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName); final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId); @@ -89,9 +91,9 @@ public class AccountTransferDetailAssembler { final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); - + final boolean backdatedTxnsAllowedTill = false; final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); - final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersReadPlatformService.java index 30c37f884..3d7d8821b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersReadPlatformService.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.account.service; import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; +import java.util.Date; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.SearchParameters; import org.apache.fineract.portfolio.account.PortfolioAccountType; @@ -41,6 +42,8 @@ public interface AccountTransfersReadPlatformService { Collection fetchPostInterestTransactionIds(Long accountId); + Collection fetchPostInterestTransactionIdsWithPivotDate(Long accountId, Date pivotDate); + AccountTransferData retrieveRefundByTransferTemplate(Long fromOfficeId, Long fromClientId, Long fromAccountId, Integer fromAccountType, Long toOfficeId, Long toClientId, Long toAccountId, Integer toAccountType); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java index 41f8a721d..e5a32636a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java @@ -26,6 +26,7 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Date; import java.util.List; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; @@ -265,6 +266,16 @@ public class AccountTransfersReadPlatformServiceImpl implements AccountTransfers return transactionId; } + @Override + public Collection fetchPostInterestTransactionIdsWithPivotDate(final Long accountId, final Date pivotDate) { + final String sql = "select att.from_savings_transaction_id from m_account_transfer_transaction att inner join m_account_transfer_details atd on atd.id = att.account_transfer_details_id where atd.from_savings_account_id=? and att.is_reversed = 0 and atd.transfer_type = ? and att.transaction_date >= ?"; + + final List transactionIds = this.jdbcTemplate.queryForList(sql, Long.class, accountId, + AccountTransferType.INTEREST_TRANSFER.getValue(), pivotDate); + + return transactionIds; + } + private static final class AccountTransfersMapper implements RowMapper { private final String schemaSql; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java index dd7ad4209..98b2162dd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java @@ -131,22 +131,24 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer boolean isAccountTransfer = true; Long fromLoanAccountId = null; boolean isWithdrawBalance = false; + final boolean backdatedTxnsAllowedTill = false; if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) { fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); - final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId); + final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, + backdatedTxnsAllowedTill); final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), isInterestTransfer, isWithdrawBalance); final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, fmt, - transactionDate, transactionAmount, paymentDetail, transactionBooleanValues); + transactionDate, transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName); - final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill); final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, - transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction); + transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); if (!fromSavingsAccount.getCurrency().getCode().equals(toSavingsAccount.getCurrency().getCode())) { throw new DifferentCurrenciesException(fromSavingsAccount.getCurrency().getCode(), @@ -161,12 +163,13 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer } else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) { // fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); - final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId); + final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, + backdatedTxnsAllowedTill); final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), isInterestTransfer, isWithdrawBalance); final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, fmt, - transactionDate, transactionAmount, paymentDetail, transactionBooleanValues); + transactionDate, transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName); final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId); @@ -194,10 +197,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null); final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); - final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, - transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction); + transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction); @@ -288,6 +291,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer Long transferTransactionId = null; final boolean isAccountTransfer = true; final boolean isRegularTransaction = accountTransferDTO.isRegularTransaction(); + final boolean backdatedTxnsAllowedTill = false; AccountTransferDetails accountTransferDetails = accountTransferDTO.getAccountTransferDetails(); if (isSavingsToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { // @@ -295,7 +299,8 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer Loan toLoanAccount = null; if (accountTransferDetails == null) { if (accountTransferDTO.getFromSavingsAccount() == null) { - fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId()); + fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId(), + backdatedTxnsAllowedTill); } else { fromSavingsAccount = accountTransferDTO.getFromSavingsAccount(); this.savingsAccountAssembler.setHelpers(fromSavingsAccount); @@ -321,7 +326,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), transactionBooleanValues); + accountTransferDTO.getPaymentDetail(), transactionBooleanValues, backdatedTxnsAllowedTill); LoanTransaction loanTransaction = null; @@ -351,13 +356,14 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer SavingsAccount toSavingsAccount = null; if (accountTransferDetails == null) { if (accountTransferDTO.getFromSavingsAccount() == null) { - fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId()); + fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId(), + backdatedTxnsAllowedTill); } else { fromSavingsAccount = accountTransferDTO.getFromSavingsAccount(); this.savingsAccountAssembler.setHelpers(fromSavingsAccount); } if (accountTransferDTO.getToSavingsAccount() == null) { - toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); + toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId(), false); } else { toSavingsAccount = accountTransferDTO.getToSavingsAccount(); this.savingsAccountAssembler.setHelpers(toSavingsAccount); @@ -383,11 +389,11 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), transactionBooleanValues); + accountTransferDTO.getPaymentDetail(), transactionBooleanValues, backdatedTxnsAllowedTill); final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction); + accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(accountTransferDTO, fromSavingsAccount, toSavingsAccount, withdrawal, deposit); @@ -405,7 +411,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer fromLoanAccount = accountTransferDTO.getLoan(); this.loanAccountAssembler.setHelpers(fromLoanAccount); } - toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); + toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill); } else { fromLoanAccount = accountTransferDetails.fromLoanAccount(); this.loanAccountAssembler.setHelpers(fromLoanAccount); @@ -426,7 +432,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction); + accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(accountTransferDTO, fromLoanAccount, toSavingsAccount, deposit, loanTransaction); this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); @@ -523,6 +529,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); BigDecimal overpaid = this.loanReadPlatformService.retrieveTotalPaidInAdvance(fromLoanAccountId).getPaidInAdvance(); + final boolean backdatedTxnsAllowedTill = false; if (overpaid == null || overpaid.compareTo(BigDecimal.ZERO) == 0 ? Boolean.TRUE : Boolean.FALSE || transactionAmount.floatValue() > overpaid.floatValue()) { @@ -536,10 +543,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null); final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); - final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, - transactionAmount, paymentDetail, true, true); + transactionAmount, paymentDetail, true, true, backdatedTxnsAllowedTill); final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java index 1e82c8ecf..0d7e451d1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java @@ -111,12 +111,14 @@ public class GuarantorWritePlatformServiceJpaRepositoryIImpl implements Guaranto guarantorCommand.validateForCreate(); validateLoanStatus(loan); final List guarantorFundingDetails = new ArrayList<>(); + final boolean backdatedTxnsAllowedTill = false; AccountAssociations accountAssociations = null; if (guarantorCommand.getSavingsId() != null) { - final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(guarantorCommand.getSavingsId()); + final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(guarantorCommand.getSavingsId(), + backdatedTxnsAllowedTill); validateGuarantorSavingsAccountActivationDateWithLoanSubmittedOnDate(loan, savingsAccount); accountAssociations = AccountAssociations.associateSavingsAccount(loan, savingsAccount, - AccountAssociationType.GUARANTOR_ACCOUNT_ASSOCIATION.getValue(), true); + AccountAssociationType.GUARANTOR_ACCOUNT_ASSOCIATION.getValue(), backdatedTxnsAllowedTill); GuarantorFundingDetails fundingDetails = new GuarantorFundingDetails(accountAssociations, GuarantorFundStatusType.ACTIVE.getValue(), guarantorCommand.getAmount()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 1235dd877..a0658d0a1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -533,6 +533,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa // Save linked account information SavingsAccount savingsAccount; + final boolean backdatedTxnsAllowedTill = false; AccountAssociations accountAssociations; final Long savingsAccountId = command.longValueOfParameterNamed("linkAccountId"); if (savingsAccountId != null) { @@ -551,8 +552,9 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } if (gsimClientMembers.contains(BigDecimal.valueOf(newLoanApplication.getClientId()))) { - savingsAccount = this.savingsAccountAssembler - .assembleFrom(clientAccountMappings.get(BigDecimal.valueOf(newLoanApplication.getClientId())).longValue()); + savingsAccount = this.savingsAccountAssembler.assembleFrom( + clientAccountMappings.get(BigDecimal.valueOf(newLoanApplication.getClientId())).longValue(), + backdatedTxnsAllowedTill); this.fromApiJsonDeserializer.validatelinkedSavingsAccount(savingsAccount, newLoanApplication); boolean isActive = true; @@ -565,7 +567,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } } else { - savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId); + savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId, backdatedTxnsAllowedTill); this.fromApiJsonDeserializer.validatelinkedSavingsAccount(savingsAccount, newLoanApplication); boolean isActive = true; accountAssociations = AccountAssociations.associateSavingsAccount(newLoanApplication, savingsAccount, @@ -1156,6 +1158,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa // Save linked account information final String linkAccountIdParamName = "linkAccountId"; + final boolean backdatedTxnsAllowedTill = false; final Long savingsAccountId = command.longValueOfParameterNamed(linkAccountIdParamName); AccountAssociations accountAssociations = this.accountAssociationsRepository.findByLoanIdAndType(loanId, AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue()); @@ -1181,7 +1184,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } } if (isModified) { - final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId); + final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId, + backdatedTxnsAllowedTill); this.fromApiJsonDeserializer.validatelinkedSavingsAccount(savingsAccount, existingLoanApplication); if (accountAssociations == null) { boolean isActive = true; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java index 1113571d8..04c043700 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java @@ -86,6 +86,7 @@ public class SavingsApiConstants { public static final String transactionLowerLimitParamName = "transactionLowerLimit"; public static final String retailEntriesParamName = "retailEntries"; public static final String childAccountIdParamName = "childAccountId"; + public static final String interestPostedTillDate = "interestPostedTillDate"; public static final String groupIdParamName = "groupId"; public static final String productIdParamName = "productId"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsApiSetConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsApiSetConstants.java index d27816704..bce7d2696 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsApiSetConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsApiSetConstants.java @@ -57,7 +57,7 @@ public class SavingsApiSetConstants extends SavingsApiConstants { "interestCompoundingPeriodTypeOptions", "interestPostingPeriodTypeOptions", "interestCalculationTypeOptions", "interestCalculationDaysInYearTypeOptions", "lockinPeriodFrequencyTypeOptions", "withdrawalFeeTypeOptions", "withdrawalFee", "annualFee", onHoldFundsParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, - datatables, savingsAmountOnHold, accountMappingForPaymentParamName)); + datatables, savingsAmountOnHold, accountMappingForPaymentParamName, interestPostedTillDate)); protected static final Set SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>( Arrays.asList(idParamName, "accountId", accountNoParamName, "currency", "amount", dateParamName, paymentDetailDataParamName, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java index 79bea0b9a..db2bd26a7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java @@ -44,12 +44,14 @@ public class SavingsAccountSummaryData implements Serializable { private final BigDecimal interestNotPosted; private final LocalDate lastInterestCalculationDate; private final BigDecimal availableBalance; + private final LocalDate interestPostedTillDate; public SavingsAccountSummaryData(final CurrencyData currency, final BigDecimal totalDeposits, final BigDecimal totalWithdrawals, final BigDecimal totalWithdrawalFees, final BigDecimal totalAnnualFees, final BigDecimal totalInterestEarned, final BigDecimal totalInterestPosted, final BigDecimal accountBalance, final BigDecimal totalFeeCharge, final BigDecimal totalPenaltyCharge, final BigDecimal totalOverdraftInterestDerived, final BigDecimal totalWithholdTax, - final BigDecimal interestNotPosted, final LocalDate lastInterestCalculationDate, final BigDecimal availableBalance) { + final BigDecimal interestNotPosted, final LocalDate lastInterestCalculationDate, final BigDecimal availableBalance, + final LocalDate interestPostedTillDate) { this.currency = currency; this.totalDeposits = totalDeposits; this.totalWithdrawals = totalWithdrawals; @@ -65,5 +67,6 @@ public class SavingsAccountSummaryData implements Serializable { this.interestNotPosted = interestNotPosted; this.lastInterestCalculationDate = lastInterestCalculationDate; this.availableBalance = availableBalance; + this.interestPostedTillDate = interestPostedTillDate; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDataValidator.java index e17ff5232..c4759943a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDataValidator.java @@ -44,6 +44,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; @@ -56,6 +57,7 @@ import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; +import org.apache.fineract.portfolio.savings.exception.TransactionBeforePivotDateNotAllowed; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -67,10 +69,31 @@ public class SavingsAccountTransactionDataValidator { private static final Set SAVINGS_ACCOUNT_HOLD_AMOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>( Arrays.asList(transactionDateParamName, SavingsApiConstants.dateFormatParamName, SavingsApiConstants.localeParamName, transactionAmountParamName)); + private final ConfigurationDomainService configurationDomainService; @Autowired - public SavingsAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) { + public SavingsAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper, + final ConfigurationDomainService configurationDomainService) { this.fromApiJsonHelper = fromApiJsonHelper; + this.configurationDomainService = configurationDomainService; + } + + public void validateTransactionWithPivotDate(final LocalDate transactionDate, final SavingsAccount savingsAccount) { + + final boolean backdatedTxnsAllowedTill = this.configurationDomainService.retrievePivotDateConfig(); + final boolean isRelaxingDaysConfigOn = this.configurationDomainService.isRelaxingDaysConfigForPivotDateEnabled(); + + final Date lastInterestPostingDate = savingsAccount.getSummary().getInterestPostedTillDate(); + + if (backdatedTxnsAllowedTill && lastInterestPostingDate != null) { + LocalDate pivotDate = LocalDate.ofInstant(lastInterestPostingDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()); + if (isRelaxingDaysConfigOn) { + pivotDate = pivotDate.minusDays(this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate()); + } + if (pivotDate.isAfter(transactionDate)) { + throw new TransactionBeforePivotDateNotAllowed(transactionDate, pivotDate); + } + } } public void validate(final JsonCommand command) { @@ -181,8 +204,8 @@ public class SavingsAccountTransactionDataValidator { } - public SavingsAccountTransaction validateHoldAndAssembleForm(final String json, final SavingsAccount account, - final AppUser createdUser) { + public SavingsAccountTransaction validateHoldAndAssembleForm(final String json, final SavingsAccount account, final AppUser createdUser, + final boolean backdatedTxnsAllowedTill) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } @@ -210,7 +233,15 @@ public class SavingsAccountTransactionDataValidator { if (account.getWithdrawableBalance().compareTo(BigDecimal.ZERO) < 0) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("insufficient balance", account.getId()); } - LocalDate lastTransactionDate = account.retrieveLastTransactionDate(); + + LocalDate lastTransactionDate = null; + + if (!backdatedTxnsAllowedTill) { + lastTransactionDate = account.retrieveLastTransactionDate(); + } else { + lastTransactionDate = account.retrieveLastTransactionDateWithPivotConfig(); + } + // compare two dates now if (lastTransactionDate != null && transactionDate.isBefore(lastTransactionDate)) { baseDataValidator.parameter(SavingsApiConstants.dateParamName).value(lastTransactionDate).failWithCode( diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java index 62c9f7af7..d1ad10b99 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java @@ -115,11 +115,11 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi boolean isAccountTransfer = false; boolean isInterestTransfer = false; boolean isWithdrawBalance = false; - + final boolean backdatedTxnsAllowedTill = false; SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, isRegularTransaction, applyWithdrawFee, isInterestTransfer, isWithdrawBalance); return this.savingsAccountDomainService.handleWithdrawal(account, fmt, transactionDate, transactionAmount, paymentDetail, - transactionBooleanValues); + transactionBooleanValues, backdatedTxnsAllowedTill); } @Transactional @@ -128,8 +128,9 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail) { boolean isAccountTransfer = false; boolean isRegularTransaction = false; + final boolean backdatedTxnsAllowedTill = false; return this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, - isAccountTransfer, isRegularTransaction); + isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); } @Transactional @@ -146,8 +147,9 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi final boolean isPreMatureClosure = false; final MathContext mc = MathContext.DECIMAL64; account.updateDepositAmount(transactionAmount); + final boolean backdatedTxnsAllowedTill = false; final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, - transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction); + transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); final boolean isAnyActivationChargesDue = isAnyActivationChargesDue(account); @@ -172,8 +174,9 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final boolean isRegularTransaction) { boolean isAccountTransfer = false; + final boolean backdatedTxnsAllowedTill = false; final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, - transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction); + transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); @@ -575,10 +578,11 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi final Set existingReversedTransactionIds, boolean isAccountTransfer) { final MonetaryCurrency currency = savingsAccount.getCurrency(); + final boolean backdatedTxnsAllowedTill = false; final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepositoryWrapper.findOneWithNotFoundDetection(currency); final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(applicationCurrency.toData(), - existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); + existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index 08988cebf..c8385ca0e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -527,8 +527,9 @@ public class FixedDepositAccount extends SavingsAccount { final MathContext mc = MathContext.DECIMAL64; final boolean isInterestTransfer = false; final LocalDate postInterestOnDate = null; + final boolean backdatedTxnsAllowedTill = false; final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate); + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); Money interestPostedToDate = Money.zero(this.currency); @@ -562,11 +563,12 @@ public class FixedDepositAccount extends SavingsAccount { } } } - recalucateDailyBalanceDetails = applyWithholdTaxForDepositAccounts(interestPostingUpToDate, recalucateDailyBalanceDetails); + recalucateDailyBalanceDetails = applyWithholdTaxForDepositAccounts(interestPostingUpToDate, recalucateDailyBalanceDetails, + backdatedTxnsAllowedTill); if (recalucateDailyBalanceDetails) { // update existing transactions so derived balance fields are // correct. - recalculateDailyBalances(Money.zero(this.currency), interestPostingUpToDate); + recalculateDailyBalances(Money.zero(this.currency), interestPostingUpToDate, backdatedTxnsAllowedTill); } this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); @@ -585,6 +587,7 @@ public class FixedDepositAccount extends SavingsAccount { // post remaining interest final Money remainigInterestToBePosted = interestOnMaturity.minus(interestPostedToDate); + final boolean backdatedTxnsAllowedTill = false; if (!remainigInterestToBePosted.isZero()) { final boolean postInterestAsOn = false; final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), @@ -593,12 +596,12 @@ public class FixedDepositAccount extends SavingsAccount { recalucateDailyBalance = true; } - recalucateDailyBalance = applyWithholdTaxForDepositAccounts(accountCloseDate, recalucateDailyBalance); + recalucateDailyBalance = applyWithholdTaxForDepositAccounts(accountCloseDate, recalucateDailyBalance, backdatedTxnsAllowedTill); if (recalucateDailyBalance) { // update existing transactions so derived balance fields are // correct. - recalculateDailyBalances(Money.zero(this.currency), accountCloseDate); + recalculateDailyBalances(Money.zero(this.currency), accountCloseDate, backdatedTxnsAllowedTill); } this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); this.accountTermAndPreClosure.updateMaturityDetails(this.getAccountBalance(), accountCloseDate); @@ -639,19 +642,19 @@ public class FixedDepositAccount extends SavingsAccount { @Override public void postInterest(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postInterestOnDate) { + final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill) { final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); super.postInterest(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); } @Override public List calculateInterestUsing(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postAsInterestOn) { + final LocalDate postAsInterestOn, final boolean backdatedTxnsAllowedTill) { final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); return super.calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postAsInterestOn); + financialYearBeginningMonth, postAsInterestOn, backdatedTxnsAllowedTill); } private LocalDate interestPostingUpToDate(final LocalDate interestPostingDate) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java index 4a00c7799..c86156038 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java @@ -539,14 +539,15 @@ public class RecurringDepositAccount extends SavingsAccount { protected void processAccountUponActivation(final DateTimeFormatter fmt, final AppUser user) { final Money minRequiredOpeningBalance = Money.of(this.currency, this.minRequiredOpeningBalance); + final boolean backdatedTxnsAllowedTill = false; if (minRequiredOpeningBalance.isGreaterThanZero()) { final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, getActivationLocalDate(), minRequiredOpeningBalance.getAmount(), null, new Date(), user, accountType); - deposit(transactionDTO); + deposit(transactionDTO, backdatedTxnsAllowedTill); // update existing transactions so derived balance fields are // correct. - recalculateDailyBalances(Money.zero(this.currency), DateUtils.getLocalDateOfTenant()); + recalculateDailyBalances(Money.zero(this.currency), DateUtils.getLocalDateOfTenant(), backdatedTxnsAllowedTill); } } @@ -640,8 +641,9 @@ public class RecurringDepositAccount extends SavingsAccount { final MathContext mc = MathContext.DECIMAL64; boolean isInterestTransfer = false; LocalDate postInterestOnDate = null; + final boolean backdatedTxnsAllowedTill = false; final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate.minusDays(1), isInterestTransfer, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate); + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); Money interestPostedToDate = Money.zero(this.currency); @@ -673,11 +675,11 @@ public class RecurringDepositAccount extends SavingsAccount { } } } - applyWithholdTaxForDepositAccounts(interestPostingUpToDate, recalucateDailyBalanceDetails); + applyWithholdTaxForDepositAccounts(interestPostingUpToDate, recalucateDailyBalanceDetails, backdatedTxnsAllowedTill); if (recalucateDailyBalanceDetails) { // update existing transactions so derived balance fields are // correct. - recalculateDailyBalances(Money.zero(this.currency), interestPostingUpToDate); + recalculateDailyBalances(Money.zero(this.currency), interestPostingUpToDate, backdatedTxnsAllowedTill); } this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); } @@ -693,6 +695,7 @@ public class RecurringDepositAccount extends SavingsAccount { financialYearBeginningMonth); boolean recalucateDailyBalance = false; + final boolean backdatedTxnsAllowedTill = false; // post remaining interest final Money remainigInterestToBePosted = interestOnMaturity.minus(interestPostedToDate); @@ -704,12 +707,12 @@ public class RecurringDepositAccount extends SavingsAccount { recalucateDailyBalance = true; } - applyWithholdTaxForDepositAccounts(accountCloseDate, recalucateDailyBalance); + applyWithholdTaxForDepositAccounts(accountCloseDate, recalucateDailyBalance, backdatedTxnsAllowedTill); if (recalucateDailyBalance) { // update existing transactions so derived balance fields are // correct. - recalculateDailyBalances(Money.zero(this.currency), accountCloseDate); + recalculateDailyBalances(Money.zero(this.currency), accountCloseDate, backdatedTxnsAllowedTill); } this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); @@ -750,19 +753,19 @@ public class RecurringDepositAccount extends SavingsAccount { @Override public void postInterest(final MathContext mc, final LocalDate postingDate, final boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postInterestAson) { + final LocalDate postInterestAson, final boolean backdatedTxnsAllowedTill) { final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); super.postInterest(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestAson); + financialYearBeginningMonth, postInterestAson, backdatedTxnsAllowedTill); } @Override public List calculateInterestUsing(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postAsInterestOn) { + final LocalDate postAsInterestOn, final boolean backdatedTxnsAllowedTill) { final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); return super.calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postAsInterestOn); + financialYearBeginningMonth, postAsInterestOn, backdatedTxnsAllowedTill); } private LocalDate interestPostingUpToDate(final LocalDate interestPostingDate) { @@ -834,7 +837,7 @@ public class RecurringDepositAccount extends SavingsAccount { } @Override - public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO) { + public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO, final boolean backdatedTxnsAllowedTill) { if (isAccountMatured()) { final String defaultUserMessage = "Transaction is not allowed. Account is matured."; @@ -872,7 +875,7 @@ public class RecurringDepositAccount extends SavingsAccount { throw new PlatformApiDataValidationException(dataValidationErrors); } - final SavingsAccountTransaction transaction = super.deposit(transactionDTO); + final SavingsAccountTransaction transaction = super.deposit(transactionDTO, backdatedTxnsAllowedTill); return transaction; } 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 a33c8e341..94192df2f 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 @@ -316,6 +316,8 @@ public class SavingsAccount extends AbstractPersistableCustom { protected SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; @Transient protected SavingsHelper savingsHelper; + @Transient + protected List savingsAccountTransactions = new ArrayList<>(); @Column(name = "deposit_type_enum", insertable = false, updatable = false) private Integer depositType; @@ -445,6 +447,14 @@ public class SavingsAccount extends AbstractPersistableCustom { this.savingsHelper = savingsHelper; } + public void setSavingsAccountTransactions(final List savingsAccountTransactions) { + this.savingsAccountTransactions.addAll(savingsAccountTransactions); + } + + public List getSavingsAccountTransactionsWithPivotConfig() { + return this.savingsAccountTransactions; + } + public String getExternalId() { return externalId; } @@ -487,15 +497,25 @@ public class SavingsAccount extends AbstractPersistableCustom { public void postInterest(final MathContext mc, final LocalDate interestPostingUpToDate, final boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postInterestOnDate) { + final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill) { final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate); + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); + Money interestPostedToDate = Money.zero(this.currency); + if (backdatedTxnsAllowedTill) { + interestPostedToDate = Money.of(this.currency, this.summary.getTotalInterestPosted()); + } + boolean recalucateDailyBalanceDetails = false; boolean applyWithHoldTax = isWithHoldTaxApplicableForInterestPosting(); final List withholdTransactions = new ArrayList<>(); - withholdTransactions.addAll(findWithHoldTransactions()); + + if (backdatedTxnsAllowedTill) { + withholdTransactions.addAll(findWithHoldSavingsTransactionsWithPivotConfig()); + } else { + withholdTransactions.addAll(findWithHoldTransactions()); + } for (final PostingPeriod interestPostingPeriod : postingPeriods) { @@ -516,9 +536,14 @@ public class SavingsAccount extends AbstractPersistableCustom { newPostingTransaction = SavingsAccountTransaction.overdraftInterest(this, office(), interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), interestPostingPeriod.isUserPosting()); } - addTransaction(newPostingTransaction); + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(newPostingTransaction); + } else { + addTransaction(newPostingTransaction); + } if (applyWithHoldTax) { - createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate); + createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, + backdatedTxnsAllowedTill); } recalucateDailyBalanceDetails = true; } else { @@ -547,9 +572,14 @@ public class SavingsAccount extends AbstractPersistableCustom { interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), interestPostingPeriod.isUserPosting()); } - addTransaction(newPostingTransaction); + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(newPostingTransaction); + } else { + addTransaction(newPostingTransaction); + } if (applyWithHoldTaxForOldTransaction) { - createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate); + createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, + backdatedTxnsAllowedTill); } recalucateDailyBalanceDetails = true; } @@ -561,14 +591,27 @@ public class SavingsAccount extends AbstractPersistableCustom { // no openingBalance concept supported yet but probably will to // allow // for migrations. - final Money openingAccountBalance = Money.zero(this.currency); + Money openingAccountBalance = Money.zero(this.currency); + + if (backdatedTxnsAllowedTill) { + if (this.summary.getLastInterestCalculationDate() == null) { + openingAccountBalance = Money.zero(this.currency); + } else { + openingAccountBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); + } + } // update existing transactions so derived balance fields are // correct. - recalculateDailyBalances(openingAccountBalance, interestPostingUpToDate); + recalculateDailyBalances(openingAccountBalance, interestPostingUpToDate, backdatedTxnsAllowedTill); } - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + if (!backdatedTxnsAllowedTill) { + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } else { + this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, null, + this.savingsAccountTransactions); + } } protected List findWithHoldTransactions() { @@ -582,6 +625,17 @@ public class SavingsAccount extends AbstractPersistableCustom { return withholdTransactions; } + protected List findWithHoldSavingsTransactionsWithPivotConfig() { + final List withholdTransactions = new ArrayList<>(); + List trans = getSavingsAccountTransactionsWithPivotConfig(); + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isWithHoldTaxAndNotReversed()) { + withholdTransactions.add(transaction); + } + } + return withholdTransactions; + } + private boolean isWithHoldTaxApplicableForInterestPosting() { return this.withHoldTax() && this.depositAccountType().isSavingsDeposit(); } @@ -599,6 +653,19 @@ public class SavingsAccount extends AbstractPersistableCustom { return postingTransation; } + protected SavingsAccountTransaction findInterestPostingSavingsTransactionWithPivotConfig(final LocalDate postingDate) { + SavingsAccountTransaction postingTransation = null; + List trans = getSavingsAccountTransactionsWithPivotConfig(); + for (final SavingsAccountTransaction transaction : trans) { + if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) + && transaction.occursOn(postingDate)) { + postingTransation = transaction; + break; + } + } + return postingTransation; + } + protected SavingsAccountTransaction findTransactionFor(final LocalDate postingDate, final List transactions) { SavingsAccountTransaction transaction = null; @@ -611,7 +678,7 @@ public class SavingsAccount extends AbstractPersistableCustom { return transaction; } - protected boolean createWithHoldTransaction(final BigDecimal amount, final LocalDate date) { + protected boolean createWithHoldTransaction(final BigDecimal amount, final LocalDate date, final boolean backdatedTxnsAllowedTill) { boolean isTaxAdded = false; if (this.taxGroup != null && amount.compareTo(BigDecimal.ZERO) > 0) { Map taxSplit = TaxUtils.splitTax(amount, date, this.taxGroup.getTaxGroupMappings(), amount.scale()); @@ -619,7 +686,11 @@ public class SavingsAccount extends AbstractPersistableCustom { if (totalTax.compareTo(BigDecimal.ZERO) > 0) { SavingsAccountTransaction withholdTransaction = SavingsAccountTransaction.withHoldTax(this, office(), date, Money.of(currency, totalTax), taxSplit); - addTransaction(withholdTransaction); + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(withholdTransaction); + } else { + addTransaction(withholdTransaction); + } isTaxAdded = true; } } @@ -665,6 +736,18 @@ public class SavingsAccount extends AbstractPersistableCustom { return savingsTransaction; } + protected SavingsAccountTransaction findLastFilteredTransactionWithPivotConfig(final LocalDate date) { + SavingsAccountTransaction savingsTransaction = null; + List trans = getSavingsAccountTransactionsWithPivotConfig(); + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isNotReversed() && transaction.occursOn(date)) { + savingsTransaction = transaction; + break; + } + } + return savingsTransaction; + } + public List getManualPostingDates() { List transactions = new ArrayList<>(); for (SavingsAccountTransaction trans : this.transactions) { @@ -675,6 +758,16 @@ public class SavingsAccount extends AbstractPersistableCustom { return transactions; } + public List getManualPostingDatesWithPivotConfig() { + List transactions = new ArrayList<>(); + for (SavingsAccountTransaction trans : this.savingsAccountTransactions) { + if (trans.isInterestPosting() && trans.isNotReversed() && trans.isManualTransaction()) { + transactions.add(trans.getTransactionLocalDate()); + } + } + return transactions; + } + /** * All interest calculation based on END-OF-DAY-BALANCE. * @@ -692,15 +785,22 @@ public class SavingsAccount extends AbstractPersistableCustom { public List calculateInterestUsing(final MathContext mc, final LocalDate upToInterestCalculationDate, boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postInterestOnDate) { + final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill) { // no openingBalance concept supported yet but probably will to allow // for migrations. - final Money openingAccountBalance = Money.zero(this.currency); + Money openingAccountBalance = null; + + // Check global configurations and 'pivot' date is null + if (backdatedTxnsAllowedTill) { + openingAccountBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); + } else { + openingAccountBalance = Money.zero(this.currency); + } // update existing transactions so derived balance fields are // correct. - recalculateDailyBalances(openingAccountBalance, upToInterestCalculationDate); + recalculateDailyBalances(openingAccountBalance, upToInterestCalculationDate, backdatedTxnsAllowedTill); // 1. default to calculate interest based on entire history OR // 2. determine latest 'posting period' and find interest credited to @@ -779,7 +879,13 @@ public class SavingsAccount extends AbstractPersistableCustom { isTransferInterestToOtherAccount()); this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods); - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + + if (backdatedTxnsAllowedTill) { + this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, null, + this.savingsAccountTransactions); + } else { + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } return allPostingPeriods; } @@ -808,6 +914,29 @@ public class SavingsAccount extends AbstractPersistableCustom { return orderedNonInterestPostingTransactions; } + protected List retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig() { + final List listOfTransactionsSorted = retrieveSortedTransactions(); + + final List orderedNonInterestPostingTransactions = new ArrayList<>(); + + for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { + if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) + && transaction.isNotReversed()) { + orderedNonInterestPostingTransactions.add(transaction); + } + } + orderedNonInterestPostingTransactions.sort(new SavingsAccountTransactionComparator()); + return orderedNonInterestPostingTransactions; + } + + protected List retrieveSortedTransactions() { + final List listOfTransactionsSorted = new ArrayList<>(); + listOfTransactionsSorted.addAll(this.savingsAccountTransactions); + final SavingsAccountTransactionComparator transactionComparator = new SavingsAccountTransactionComparator(); + Collections.sort(listOfTransactionsSorted, transactionComparator); + return listOfTransactionsSorted; + } + protected List retreiveListOfTransactions() { final List listOfTransactionsSorted = new ArrayList<>(); listOfTransactionsSorted.addAll(this.transactions); @@ -817,11 +946,19 @@ public class SavingsAccount extends AbstractPersistableCustom { return listOfTransactionsSorted; } - protected void recalculateDailyBalances(final Money openingAccountBalance, final LocalDate interestPostingUpToDate) { + protected void recalculateDailyBalances(final Money openingAccountBalance, final LocalDate interestPostingUpToDate, + final boolean backdatedTxnsAllowedTill) { Money runningBalance = openingAccountBalance.copy(); - List accountTransactionsSorted = retreiveListOfTransactions(); + List accountTransactionsSorted = null; + + if (backdatedTxnsAllowedTill) { + accountTransactionsSorted = retrieveSortedTransactions(); + } else { + accountTransactionsSorted = retreiveListOfTransactions(); + } + boolean isTransactionsModified = false; for (final SavingsAccountTransaction transaction : accountTransactionsSorted) { if (transaction.isReversed()) { @@ -867,7 +1004,11 @@ public class SavingsAccount extends AbstractPersistableCustom { accountTransaction.updateOverdraftAmount(overdraftAmount.getAmount()); } accountTransaction.updateRunningBalance(runningBalance); - addTransaction(accountTransaction); + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(accountTransaction); + } else { + addTransaction(accountTransaction); + } isTransactionsModified = true; } @@ -875,7 +1016,11 @@ public class SavingsAccount extends AbstractPersistableCustom { } if (isTransactionsModified) { - accountTransactionsSorted = retreiveListOfTransactions(); + if (backdatedTxnsAllowedTill) { + accountTransactionsSorted = retrieveSortedTransactions(); + } else { + accountTransactionsSorted = retreiveListOfTransactions(); + } } resetAccountTransactionsEndOfDayBalances(accountTransactionsSorted, interestPostingUpToDate); } @@ -896,16 +1041,17 @@ public class SavingsAccount extends AbstractPersistableCustom { } } - public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO) { - return deposit(transactionDTO, SavingsAccountTransactionType.DEPOSIT); + public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO, final boolean backdatedTxnsAllowedTill) { + return deposit(transactionDTO, SavingsAccountTransactionType.DEPOSIT, backdatedTxnsAllowedTill); } - public SavingsAccountTransaction dividendPayout(final SavingsAccountTransactionDTO transactionDTO) { - return deposit(transactionDTO, SavingsAccountTransactionType.DIVIDEND_PAYOUT); + public SavingsAccountTransaction dividendPayout(final SavingsAccountTransactionDTO transactionDTO, + final boolean backdatedTxnsAllowedTill) { + return deposit(transactionDTO, SavingsAccountTransactionType.DIVIDEND_PAYOUT, backdatedTxnsAllowedTill); } public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO, - final SavingsAccountTransactionType savingsAccountTransactionType) { + final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) { final String resourceTypeName = depositAccountType().resourceName(); if (isNotActive()) { final String defaultUserMessage = "Transaction is not allowed. Account is not active."; @@ -950,8 +1096,12 @@ public class SavingsAccount extends AbstractPersistableCustom { final SavingsAccountTransaction transaction = SavingsAccountTransaction.deposit(this, office(), transactionDTO.getPaymentDetail(), transactionDTO.getTransactionDate(), amount, transactionDTO.getCreatedDate(), transactionDTO.getAppUser(), savingsAccountTransactionType); - addTransaction(transaction); - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(transaction); + } else { + addTransaction(transaction); + } if (this.sub_status.equals(SavingsAccountSubStatusEnum.INACTIVE.getValue()) || this.sub_status.equals(SavingsAccountSubStatusEnum.DORMANT.getValue())) { @@ -986,7 +1136,8 @@ public class SavingsAccount extends AbstractPersistableCustom { return startInterestCalculationLocalDate; } - public SavingsAccountTransaction withdraw(final SavingsAccountTransactionDTO transactionDTO, final boolean applyWithdrawFee) { + public SavingsAccountTransaction withdraw(final SavingsAccountTransactionDTO transactionDTO, final boolean applyWithdrawFee, + final boolean backdatedTxnsAllowedTill) { if (!isTransactionsAllowed()) { @@ -1042,14 +1193,19 @@ public class SavingsAccount extends AbstractPersistableCustom { if (applyWithdrawFee) { // auto pay withdrawal fee payWithdrawalFee(transactionDTO.getTransactionAmount(), transactionDTO.getTransactionDate(), transactionDTO.getAppUser(), - transactionDTO.getPaymentDetail()); + transactionDTO.getPaymentDetail(), backdatedTxnsAllowedTill); } final Money transactionAmountMoney = Money.of(this.currency, transactionDTO.getTransactionAmount()); final SavingsAccountTransaction transaction = SavingsAccountTransaction.withdrawal(this, office(), transactionDTO.getPaymentDetail(), transactionDTO.getTransactionDate(), transactionAmountMoney, transactionDTO.getCreatedDate(), transactionDTO.getAppUser()); - addTransaction(transaction); + + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(transaction); + } else { + addTransaction(transaction); + } if (this.sub_status.equals(SavingsAccountSubStatusEnum.INACTIVE.getValue()) || this.sub_status.equals(SavingsAccountSubStatusEnum.DORMANT.getValue())) { @@ -1071,8 +1227,7 @@ public class SavingsAccount extends AbstractPersistableCustom { } private void payWithdrawalFee(final BigDecimal transactionAmount, final LocalDate transactionDate, final AppUser user, - final PaymentDetail paymentDetail) { - + final PaymentDetail paymentDetail, final boolean backdatedTxnsAllowedTill) { for (SavingsAccountCharge charge : this.charges()) { if (charge.isWithdrawalFee() && charge.isActive()) { @@ -1089,7 +1244,8 @@ public class SavingsAccount extends AbstractPersistableCustom { } else if (charge.isEnablePaymentType()) { // normal charge-transaction to specific paymentType if (paymentDetail.getPaymentType().getPaymentName().equals(charge.getCharge().getPaymentType().getPaymentName())) { charge.updateWithdralFeeAmount(transactionAmount); - this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, user); + this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, user, + backdatedTxnsAllowedTill); } } else if (!charge.isEnablePaymentType() && charge.isEnableFreeWithdrawal()) { // discount transaction // irrespective of @@ -1098,8 +1254,10 @@ public class SavingsAccount extends AbstractPersistableCustom { } else { // normal-withdraw charge.updateWithdralFeeAmount(transactionAmount); - this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, user); + this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, user, + backdatedTxnsAllowedTill); } + } } @@ -1160,13 +1318,14 @@ public class SavingsAccount extends AbstractPersistableCustom { private void countValidation(SavingsAccountCharge charge, final BigDecimal transactionAmount, final LocalDate transactionDate, final AppUser user) { + boolean backdatedTxnsAllowedTill = false; if (charge.getFreeWithdrawalCount() < charge.getFrequencyFreeWithdrawalCharge()) { final Integer count = charge.getFreeWithdrawalCount() + 1; charge.setFreeWithdrawalCount(count); charge.updateNoWithdrawalFee(); } else { charge.updateWithdralFeeAmount(transactionAmount); - this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, user); + this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, user, backdatedTxnsAllowedTill); } } @@ -1176,25 +1335,47 @@ public class SavingsAccount extends AbstractPersistableCustom { charge.updateNoWithdrawalFee(); } - public boolean isBeforeLastPostingPeriod(final LocalDate transactionDate) { + public boolean isBeforeLastPostingPeriod(final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) { boolean transactionBeforeLastInterestPosting = false; - for (final SavingsAccountTransaction transaction : retreiveListOfTransactions()) { - if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) - && transaction.isAfter(transactionDate)) { - transactionBeforeLastInterestPosting = true; - break; + if (!backdatedTxnsAllowedTill) { + for (final SavingsAccountTransaction transaction : retreiveListOfTransactions()) { + if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) + && transaction.isAfter(transactionDate)) { + transactionBeforeLastInterestPosting = true; + break; + } } + } else { + if (this.summary.getInterestPostedTillDate() == null) { + return false; + } + + transactionBeforeLastInterestPosting = LocalDate + .ofInstant(this.summary.getInterestPostedTillDate().toInstant(), DateUtils.getDateTimeZoneOfTenant()) + .isAfter(transactionDate) ? true : false; } return transactionBeforeLastInterestPosting; } public void validateAccountBalanceDoesNotBecomeNegative(final BigDecimal transactionAmount, final boolean isException, - final List depositAccountOnHoldTransactions) { - final List transactionsSortedByDate = retreiveListOfTransactions(); + final List depositAccountOnHoldTransactions, final boolean backdatedTxnsAllowedTill) { + + List transactionsSortedByDate = null; + + if (backdatedTxnsAllowedTill) { + transactionsSortedByDate = retrieveSortedTransactions(); + } else { + transactionsSortedByDate = retreiveListOfTransactions(); + } + Money runningBalance = Money.zero(this.currency); + if (backdatedTxnsAllowedTill) { + runningBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); + } + Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); LocalDate lastSavingsDate = null; final BigDecimal withdrawalFee = null; @@ -1255,10 +1436,20 @@ public class SavingsAccount extends AbstractPersistableCustom { } public void validateAccountBalanceDoesNotBecomeNegative(final String transactionAction, - final List depositAccountOnHoldTransactions) { + final List depositAccountOnHoldTransactions, final boolean backdatedTxnsAllowedTill) { - final List transactionsSortedByDate = retreiveListOfTransactions(); + List transactionsSortedByDate = null; + if (backdatedTxnsAllowedTill) { + transactionsSortedByDate = retrieveSortedTransactions(); + } else { + transactionsSortedByDate = retreiveListOfTransactions(); + } Money runningBalance = Money.zero(this.currency); + + if (backdatedTxnsAllowedTill) { + runningBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); + } + Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); LocalDate lastSavingsDate = null; for (final SavingsAccountTransaction transaction : transactionsSortedByDate) { @@ -1586,7 +1777,7 @@ public class SavingsAccount extends AbstractPersistableCustom { } public Map deriveAccountingBridgeData(final CurrencyData currencyData, final Set existingTransactionIds, - final Set existingReversedTransactionIds, boolean isAccountTransfer) { + final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) { final Map accountingBridgeData = new LinkedHashMap<>(); accountingBridgeData.put("savingsId", getId()); @@ -1598,7 +1789,16 @@ public class SavingsAccount extends AbstractPersistableCustom { accountingBridgeData.put("isAccountTransfer", isAccountTransfer); final List> newSavingsTransactions = new ArrayList<>(); - List trans = getTransactions(); + + List trans = null; + + if (backdatedTxnsAllowedTill) { + trans = getSavingsAccountTransactionsWithPivotConfig(); + } else { + trans = getTransactions(); + } + + // Adding new transactions to the array for (final SavingsAccountTransaction transaction : trans) { if (transaction.isReversed() && !existingReversedTransactionIds.contains(transaction.getId())) { newSavingsTransactions.add(transaction.toMapData(currencyData)); @@ -1612,13 +1812,21 @@ public class SavingsAccount extends AbstractPersistableCustom { } public Collection findExistingTransactionIds() { - final Collection ids = new ArrayList<>(); List trans = getTransactions(); for (final SavingsAccountTransaction transaction : trans) { ids.add(transaction.getId()); } + return ids; + } + public Collection findCurrentTransactionIdsWithPivotDateConfig() { + + final Collection ids = new ArrayList<>(); + List trans = getSavingsAccountTransactionsWithPivotConfig(); + for (final SavingsAccountTransaction transaction : trans) { + ids.add(transaction.getId()); + } return ids; } @@ -1626,6 +1834,7 @@ public class SavingsAccount extends AbstractPersistableCustom { final Collection ids = new ArrayList<>(); List trans = getTransactions(); + // time consuming for (final SavingsAccountTransaction transaction : trans) { if (transaction.isReversed()) { ids.add(transaction.getId()); @@ -1635,6 +1844,18 @@ public class SavingsAccount extends AbstractPersistableCustom { return ids; } + public Collection findCurrentReversedTransactionIdsWithPivotDateConfig() { + final Collection ids = new ArrayList<>(); + List trans = getSavingsAccountTransactionsWithPivotConfig(); + // time consuming + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isReversed()) { + ids.add(transaction.getId()); + } + } + return ids; + } + public void update(final Client client) { this.client = client; } @@ -2090,6 +2311,36 @@ public class SavingsAccount extends AbstractPersistableCustom { } } + public void undoSavingsTransaction(final Long transactionId) { + + SavingsAccountTransaction transactionToUndo = null; + for (final SavingsAccountTransaction transaction : this.savingsAccountTransactions) { + if (transaction.isIdentifiedBy(transactionId)) { + transactionToUndo = transaction; + } + } + + if (transactionToUndo == null) { + throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionId); + } + + validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_TRANSACTION, transactionToUndo.transactionLocalDate()); + transactionToUndo.reverse(); + if (transactionToUndo.isChargeTransaction() || transactionToUndo.isWaiveCharge()) { + // undo charge + final Set chargesPaidBy = transactionToUndo.getSavingsAccountChargesPaid(); + for (final SavingsAccountChargePaidBy savingsAccountChargePaidBy : chargesPaidBy) { + final SavingsAccountCharge chargeToUndo = savingsAccountChargePaidBy.getSavingsAccountCharge(); + if (transactionToUndo.isChargeTransaction()) { + chargeToUndo.undoPayment(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); + } else if (transactionToUndo.isWaiveCharge()) { + chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); + } + } + } + } + private Date findLatestAnnualFeeTransactionDueDate() { Date nextDueDate = null; @@ -2341,8 +2592,8 @@ public class SavingsAccount extends AbstractPersistableCustom { charge.updateToNextDueDateFrom(getActivationLocalDate()); } - // auto pay the activation time charges - this.payActivationCharges(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, user); + // auto pay the activation time charges (No need of checking the pivot date config) + this.payActivationCharges(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, user, false); // TODO : AA add activation charges to actual changes list } @@ -2366,12 +2617,13 @@ public class SavingsAccount extends AbstractPersistableCustom { } private void payActivationCharges(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final AppUser user) { + final AppUser user, final boolean backdatedTxnsAllowedTill) { boolean isSavingsChargeApplied = false; for (SavingsAccountCharge savingsAccountCharge : this.charges()) { if (savingsAccountCharge.isSavingsActivation()) { isSavingsChargeApplied = true; - payCharge(savingsAccountCharge, savingsAccountCharge.getAmountOutstanding(getCurrency()), getActivationLocalDate(), user); + payCharge(savingsAccountCharge, savingsAccountCharge.getAmountOutstanding(getCurrency()), getActivationLocalDate(), user, + backdatedTxnsAllowedTill); } } @@ -2379,14 +2631,14 @@ public class SavingsAccount extends AbstractPersistableCustom { final MathContext mc = MathContext.DECIMAL64; boolean isInterestTransfer = false; LocalDate postInterestAsOnDate = null; - if (this.isBeforeLastPostingPeriod(getActivationLocalDate())) { + if (this.isBeforeLastPostingPeriod(getActivationLocalDate(), backdatedTxnsAllowedTill)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); this.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestAsOnDate); + postInterestAsOnDate, backdatedTxnsAllowedTill); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); this.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestAsOnDate); + financialYearBeginningMonth, postInterestAsOnDate, backdatedTxnsAllowedTill); } } } @@ -2535,6 +2787,10 @@ public class SavingsAccount extends AbstractPersistableCustom { this.transactions.add(transaction); } + public void addTransactionToExisting(final SavingsAccountTransaction transaction) { + this.savingsAccountTransactions.add(transaction); + } + public void setStatus(final Integer status) { this.status = status; } @@ -2588,7 +2844,7 @@ public class SavingsAccount extends AbstractPersistableCustom { this.charges.remove(charge); } - public void waiveCharge(final Long savingsAccountChargeId, final AppUser user) { + public void waiveCharge(final Long savingsAccountChargeId, final AppUser user, final boolean backdatedTxnsAllowedTill) { final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) @@ -2639,7 +2895,7 @@ public class SavingsAccount extends AbstractPersistableCustom { // waive charge final Money amountWaived = savingsAccountCharge.waive(getCurrency()); - handleWaiverChargeTransactions(savingsAccountCharge, amountWaived, user); + handleWaiverChargeTransactions(savingsAccountCharge, amountWaived, user, backdatedTxnsAllowedTill); } @@ -2722,7 +2978,8 @@ public class SavingsAccount extends AbstractPersistableCustom { } public SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final BigDecimal amountPaid, - final LocalDate transactionDate, final DateTimeFormatter formatter, final AppUser user) { + final LocalDate transactionDate, final DateTimeFormatter formatter, final AppUser user, + final boolean backdatedTxnsAllowedTill) { final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) @@ -2810,17 +3067,17 @@ public class SavingsAccount extends AbstractPersistableCustom { } } - return this.payCharge(savingsAccountCharge, chargePaid, transactionDate, user); + return this.payCharge(savingsAccountCharge, chargePaid, transactionDate, user, backdatedTxnsAllowedTill); } public SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final Money amountPaid, - final LocalDate transactionDate, final AppUser user) { + final LocalDate transactionDate, final AppUser user, final boolean backdatedTxnsAllowedTill) { savingsAccountCharge.pay(getCurrency(), amountPaid); - return handlePayChargeTransactions(savingsAccountCharge, amountPaid, transactionDate, user); + return handlePayChargeTransactions(savingsAccountCharge, amountPaid, transactionDate, user, backdatedTxnsAllowedTill); } private SavingsAccountTransaction handlePayChargeTransactions(SavingsAccountCharge savingsAccountCharge, Money transactionAmount, - final LocalDate transactionDate, final AppUser user) { + final LocalDate transactionDate, final AppUser user, final boolean backdatedTxnsAllowedTill) { SavingsAccountTransaction chargeTransaction = null; if (savingsAccountCharge.isWithdrawalFee()) { @@ -2831,23 +3088,31 @@ public class SavingsAccount extends AbstractPersistableCustom { chargeTransaction = SavingsAccountTransaction.charge(this, office(), transactionDate, transactionAmount, user); } - handleChargeTransactions(savingsAccountCharge, chargeTransaction); + handleChargeTransactions(savingsAccountCharge, chargeTransaction, backdatedTxnsAllowedTill); return chargeTransaction; } - private void handleWaiverChargeTransactions(SavingsAccountCharge savingsAccountCharge, Money transactionAmount, AppUser user) { + private void handleWaiverChargeTransactions(SavingsAccountCharge savingsAccountCharge, Money transactionAmount, AppUser user, + boolean backdatedTxnsAllowedTill) { final SavingsAccountTransaction chargeTransaction = SavingsAccountTransaction.waiver(this, office(), DateUtils.getLocalDateOfTenant(), transactionAmount, user); - handleChargeTransactions(savingsAccountCharge, chargeTransaction); + handleChargeTransactions(savingsAccountCharge, chargeTransaction, backdatedTxnsAllowedTill); } - private void handleChargeTransactions(final SavingsAccountCharge savingsAccountCharge, final SavingsAccountTransaction transaction) { + private void handleChargeTransactions(final SavingsAccountCharge savingsAccountCharge, final SavingsAccountTransaction transaction, + final boolean backdatedTxnsAllowedTill) { // Provide a link between transaction and savings charge for which // amount is waived. final SavingsAccountChargePaidBy chargePaidBy = SavingsAccountChargePaidBy.instance(transaction, savingsAccountCharge, transaction.getAmount(this.getCurrency()).getAmount()); transaction.getSavingsAccountChargesPaid().add(chargePaidBy); - this.transactions.add(transaction); + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactions.add(transaction); + this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, transaction, + this.savingsAccountTransactions); + } else { + this.transactions.add(transaction); + } } private SavingsAccountCharge getCharge(final Long savingsAccountChargeId) { @@ -3069,13 +3334,14 @@ public class SavingsAccount extends AbstractPersistableCustom { this.withHoldTax = withHoldTax; } - protected boolean applyWithholdTaxForDepositAccounts(final LocalDate interestPostingUpToDate, boolean recalucateDailyBalance) { + protected boolean applyWithholdTaxForDepositAccounts(final LocalDate interestPostingUpToDate, boolean recalucateDailyBalance, + final boolean backdatedTxnsAllowedTill) { final List withholdTransactions = findWithHoldTransactions(); SavingsAccountTransaction withholdTransaction = findTransactionFor(interestPostingUpToDate, withholdTransactions); final BigDecimal totalInterestPosted = this.savingsAccountTransactionSummaryWrapper.calculateTotalInterestPosted(this.currency, this.transactions); if (withholdTransaction == null && this.withHoldTax()) { - boolean isWithholdTaxAdded = createWithHoldTransaction(totalInterestPosted, interestPostingUpToDate); + boolean isWithholdTaxAdded = createWithHoldTransaction(totalInterestPosted, interestPostingUpToDate, backdatedTxnsAllowedTill); recalucateDailyBalance = recalucateDailyBalance || isWithholdTaxAdded; } else { boolean isWithholdTaxAdded = updateWithHoldTransaction(totalInterestPosted, withholdTransaction); @@ -3085,16 +3351,16 @@ public class SavingsAccount extends AbstractPersistableCustom { return recalucateDailyBalance; } - public void setSubStatusInactive(AppUser appUser) { + public void setSubStatusInactive(AppUser appUser, final boolean backdatedTxnsAllowedTill) { this.sub_status = SavingsAccountSubStatusEnum.INACTIVE.getValue(); LocalDate transactionDate = DateUtils.getLocalDateOfTenant(); for (SavingsAccountCharge charge : this.charges()) { if (charge.isSavingsNoActivity() && charge.isActive()) { charge.updateWithdralFeeAmount(this.getAccountBalance()); - this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, appUser); + this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, appUser, backdatedTxnsAllowedTill); } } - recalculateDailyBalances(Money.zero(this.currency), transactionDate); + recalculateDailyBalances(Money.zero(this.currency), transactionDate, backdatedTxnsAllowedTill); this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); } @@ -3113,10 +3379,14 @@ public class SavingsAccount extends AbstractPersistableCustom { SavingsAccountTransaction transaction = SavingsAccountTransaction.escheat(this, transactionDate, appUser, postInterestAsOnDate); this.transactions.add(transaction); } - recalculateDailyBalances(Money.zero(this.currency), transactionDate); + recalculateDailyBalances(Money.zero(this.currency), transactionDate, false); this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); } + public void loadTransactions() { + transactions.size(); + } + public void loadLazyCollections() { transactions.size(); charges.size(); @@ -3355,6 +3625,19 @@ public class SavingsAccount extends AbstractPersistableCustom { return lastransactionDate; } + public LocalDate retrieveLastTransactionDateWithPivotConfig() { + final List transactionsSortedByDate = retrieveSortedTransactions(); + SavingsAccountTransaction lastTransaction = null; + if (transactionsSortedByDate.size() > 0) { + lastTransaction = transactionsSortedByDate.get(transactionsSortedByDate.size() - 1); + } + LocalDate lastransactionDate = null; + if (lastTransaction != null) { + lastransactionDate = lastTransaction.transactionLocalDate(); + } + return lastransactionDate; + } + public BigDecimal getSavingsHoldAmount() { return this.savingsOnHoldAmount == null ? BigDecimal.ZERO : this.savingsOnHoldAmount; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java index a1bffb149..17eab631e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java @@ -47,11 +47,15 @@ import com.google.gson.JsonElement; import java.math.BigDecimal; import java.time.LocalDate; import java.util.Arrays; +import java.util.Date; +import java.util.List; import java.util.Set; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.RoutingDataSource; import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; @@ -74,6 +78,7 @@ import org.apache.fineract.useradministration.domain.AppUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Service @@ -89,6 +94,8 @@ public class SavingsAccountAssembler { private final SavingsAccountRepositoryWrapper savingsAccountRepository; private final SavingsAccountChargeAssembler savingsAccountChargeAssembler; private final FromJsonHelper fromApiJsonHelper; + private final JdbcTemplate jdbcTemplate; + private final ConfigurationDomainService configurationDomainService; @Autowired public SavingsAccountAssembler(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, @@ -96,7 +103,8 @@ public class SavingsAccountAssembler { final StaffRepositoryWrapper staffRepository, final SavingsProductRepository savingProductRepository, final SavingsAccountRepositoryWrapper savingsAccountRepository, final SavingsAccountChargeAssembler savingsAccountChargeAssembler, final FromJsonHelper fromApiJsonHelper, - final AccountTransfersReadPlatformService accountTransfersReadPlatformService) { + final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final RoutingDataSource dataSource, + final ConfigurationDomainService configurationDomainService) { this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; this.clientRepository = clientRepository; this.groupRepository = groupRepository; @@ -106,6 +114,8 @@ public class SavingsAccountAssembler { this.savingsAccountChargeAssembler = savingsAccountChargeAssembler; this.fromApiJsonHelper = fromApiJsonHelper; savingsHelper = new SavingsHelper(accountTransfersReadPlatformService); + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.configurationDomainService = configurationDomainService; } /** @@ -307,12 +317,58 @@ public class SavingsAccountAssembler { return account; } - public SavingsAccount assembleFrom(final Long savingsId) { - final SavingsAccount account = this.savingsAccountRepository.findOneWithNotFoundDetection(savingsId); + public SavingsAccount assembleFrom(final Long savingsId, final boolean backdatedTxnsAllowedTill) { + SavingsAccount account = this.savingsAccountRepository.findSavingsWithNotFoundDetection(savingsId, backdatedTxnsAllowedTill); + return loadTransactionsToSavingsAccount(account, backdatedTxnsAllowedTill); + } + + public SavingsAccount loadTransactionsToSavingsAccount(final SavingsAccount account, final boolean backdatedTxnsAllowedTill) { + List savingsAccountTransactions = null; + if (backdatedTxnsAllowedTill) { + Date pivotDate = account.getSummary().getInterestPostedTillDate(); + boolean isNotPresent = pivotDate == null ? true : false; + if (!isNotPresent) { + // Get savings account transactions + if (isRelaxingDaysConfigForPivotDateEnabled()) { + final Long relaxingDaysForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); + LocalDate interestPostedTillDate = LocalDate.ofInstant(account.getSummary().getInterestPostedTillDate().toInstant(), + DateUtils.getDateTimeZoneOfTenant()); + savingsAccountTransactions = this.savingsAccountRepository.findTransactionsAfterPivotDate(account, + Date.from(interestPostedTillDate.minusDays(relaxingDaysForPivotDate) + .atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant())); + } else { + savingsAccountTransactions = this.savingsAccountRepository.findTransactionsAfterPivotDate(account, + account.getSummary().getInterestPostedTillDate()); + } + + if (savingsAccountTransactions != null && savingsAccountTransactions.size() > 0) { + // Update transient variable + account.setSavingsAccountTransactions(savingsAccountTransactions); + } + } else { + savingsAccountTransactions = this.savingsAccountRepository.findAllTransactions(account); + account.setSavingsAccountTransactions(savingsAccountTransactions); + } + } + + // Update last running balance on account level + if (savingsAccountTransactions != null) { + account.getSummary().setRunningBalanceOnPivotDate(savingsAccountTransactions.get(savingsAccountTransactions.size() - 1) + .getRunningBalance(account.getCurrency()).getAmount()); + } + account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); return account; } + public boolean getPivotConfigStatus() { + return this.configurationDomainService.retrievePivotDateConfig(); + } + + public boolean isRelaxingDaysConfigForPivotDateEnabled() { + return this.configurationDomainService.isRelaxingDaysConfigForPivotDateEnabled(); + } + public void setHelpers(final SavingsAccount account) { account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainService.java index 735406f54..8b656858d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainService.java @@ -28,13 +28,16 @@ import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; public interface SavingsAccountDomainService { SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, - BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues); + BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues, + boolean backdatedTxnsAllowedTill); SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, - BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction); + BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction, + boolean backdatedTxnsAllowedTill); - void postJournalEntries(SavingsAccount savingsAccount, Set existingTransactionIds, Set existingReversedTransactionIds); - - SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount); + void postJournalEntries(SavingsAccount savingsAccount, Set existingTransactionIds, Set existingReversedTransactionIds, + boolean backdatedTxnsAllowedTill); + SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount, + boolean backdatedTxnsAllowedTill); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java index 81ead6988..8b784894f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java @@ -82,7 +82,7 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi @Override public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final SavingsTransactionBooleanValues transactionBooleanValues) { + final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) { AppUser user = getAppUserIfPresent(); account.validateForAccountBlock(); @@ -96,32 +96,46 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi final Set existingTransactionIds = new HashSet<>(); final LocalDate postInterestOnDate = null; final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + if (backdatedTxnsAllowedTill) { + updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + Integer accountType = null; final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, paymentDetail, new Date(), user, accountType); - final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee()); + final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(), + backdatedTxnsAllowedTill); final MathContext mc = MathContext.DECIMAL64; - if (account.isBeforeLastPostingPeriod(transactionDate)) { + + if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(), - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate); + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); } + List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); } + account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(), - depositAccountOnHoldTransactions); + depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); + saveTransactionToGenerateTransactionId(withdrawal); + this.savingsAccountRepository.save(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer()); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer(), + backdatedTxnsAllowedTill); + this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.SAVINGS_WITHDRAWAL, constructEntityMap(BusinessEntity.SAVINGS_TRANSACTION, withdrawal)); return withdrawal; @@ -139,19 +153,21 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi @Override public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final boolean isAccountTransfer, final boolean isRegularTransaction) { + final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) { final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT; return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, - savingsAccountTransactionType); + savingsAccountTransactionType, backdatedTxnsAllowedTill); } private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final boolean isAccountTransfer, final boolean isRegularTransaction, - final SavingsAccountTransactionType savingsAccountTransactionType) { + final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) { AppUser user = getAppUserIfPresent(); account.validateForAccountBlock(); account.validateForCreditBlock(); + + // Global configurations final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); @@ -162,27 +178,40 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi boolean isInterestTransfer = false; final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + if (backdatedTxnsAllowedTill) { + updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + Integer accountType = null; final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, paymentDetail, new Date(), user, accountType); - final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType); + final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill); final LocalDate postInterestOnDate = null; final MathContext mc = MathContext.DECIMAL64; - if (account.isBeforeLastPostingPeriod(transactionDate)) { + + if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, backdatedTxnsAllowedTill); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); } + saveTransactionToGenerateTransactionId(deposit); + if (backdatedTxnsAllowedTill) { + // Update transactions separately + saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig()); + } + this.savingsAccountRepository.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.SAVINGS_DEPOSIT, constructEntityMap(BusinessEntity.SAVINGS_TRANSACTION, deposit)); return deposit; @@ -190,19 +219,14 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi @Override public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate, - final BigDecimal transactionAmount) { + final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) { final DateTimeFormatter fmt = null; final PaymentDetail paymentDetail = null; final boolean isAccountTransfer = false; final boolean isRegularTransaction = true; final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT; return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, - savingsAccountTransactionType); - } - - private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { - this.savingsAccountTransactionRepository.save(transaction); - return transaction.getId(); + savingsAccountTransactionType, backdatedTxnsAllowedTill); } private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, @@ -211,24 +235,39 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); } + private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { + this.savingsAccountTransactionRepository.save(transaction); + return transaction.getId(); + } + + private void saveUpdatedTransactionsOfSavingsAccount(final List savingsAccountTransactions) { + this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions); + } + + private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); + existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); + } + private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds, boolean isAccountTransfer) { + final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) { final MonetaryCurrency currency = savingsAccount.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepositoryWrapper.findOneWithNotFoundDetection(currency); final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(applicationCurrency.toData(), - existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); + existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); } @Transactional @Override public void postJournalEntries(final SavingsAccount account, final Set existingTransactionIds, - final Set existingReversedTransactionIds) { + final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { final boolean isAccountTransfer = false; - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); } private Map constructEntityMap(final BusinessEntity entityEvent, Object entity) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java index 0de9ce8d4..f7454b573 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java @@ -19,10 +19,12 @@ package org.apache.fineract.portfolio.savings.domain; import java.util.List; +import javax.persistence.LockModeType; import org.springframework.data.domain.Page; 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; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -32,6 +34,10 @@ public interface SavingsAccountRepository extends JpaRepository findSavingAccountByClientId(@Param("clientId") Long clientId); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select sa from SavingsAccount sa where sa.id = :savingsId") + SavingsAccount findOneLocked(@Param("savingsId") Long id); + @Query("select s_acc from SavingsAccount s_acc where s_acc.gsim.id = :gsimId") List findSavingAccountByGsimId(@Param("gsimId") Long gsimId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java index 2ce045b60..f031a0c6b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java @@ -18,13 +18,16 @@ */ package org.apache.fineract.portfolio.savings.domain; +import java.util.Date; import java.util.List; +import org.apache.fineract.infrastructure.core.service.RoutingDataSource; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.query.Param; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,10 +46,15 @@ import org.springframework.transaction.annotation.Transactional; public class SavingsAccountRepositoryWrapper { private final SavingsAccountRepository repository; + private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; + private final JdbcTemplate jdbcTemplate; @Autowired - public SavingsAccountRepositoryWrapper(final SavingsAccountRepository repository) { + public SavingsAccountRepositoryWrapper(final SavingsAccountRepository repository, + final SavingsAccountTransactionRepository savingsAccountTransactionRepository, final RoutingDataSource dataSource) { this.repository = repository; + this.savingsAccountTransactionRepository = savingsAccountTransactionRepository; + this.jdbcTemplate = new JdbcTemplate(dataSource); } @Transactional(readOnly = true) @@ -57,6 +65,21 @@ public class SavingsAccountRepositoryWrapper { return account; } + @Transactional + public SavingsAccount findSavingsWithNotFoundDetection(final Long savingsId, final boolean backdatedTxnsAllowedTill) { + SavingsAccount account = null; + if (!backdatedTxnsAllowedTill) { + account = findOneWithNotFoundDetection(savingsId); + } else { + account = this.repository.findOneLocked(savingsId); + if (account == null) { + throw new SavingsAccountNotFoundException(savingsId); + } + } + + return account; + } + @Transactional(readOnly = true) public SavingsAccount findOneWithNotFoundDetection(final Long savingsId, final DepositAccountType depositAccountType) { final SavingsAccount account = this.repository.findByIdAndDepositAccountType(savingsId, depositAccountType.getValue()); @@ -106,6 +129,17 @@ public class SavingsAccountRepositoryWrapper { return this.repository.findSavingAccountByGsimId(gsimId); } + @Transactional + public List findTransactionsAfterPivotDate(@Param("savingsAccount") SavingsAccount savingsAccount, + @Param("date") Date date) { + return this.savingsAccountTransactionRepository.findTransactionsAfterPivotDate(savingsAccount, date); + } + + @Transactional + public List findAllTransactions(@Param("savingsAccount") SavingsAccount savingsAccount) { + return this.savingsAccountTransactionRepository.findBySavingsAccount(savingsAccount); + } + // Root Entity is enough public SavingsAccount findNonClosedAccountByAccountNumber(@Param("accountNumber") String accountNumber) { return this.repository.findNonClosedAccountByAccountNumber(accountNumber); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java index ef9f5accf..9bddc61fd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java @@ -22,6 +22,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.ZoneId; import java.util.Date; +import java.util.HashMap; import java.util.List; import javax.persistence.Column; import javax.persistence.Embeddable; @@ -31,6 +32,7 @@ import javax.persistence.Transient; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; /** @@ -83,6 +85,14 @@ public final class SavingsAccountSummary { @Column(name = "last_interest_calculation_date") private Date lastInterestCalculationDate; + // Currently this represents the last interest posting date + @Temporal(TemporalType.DATE) + @Column(name = "interest_posted_till_date") + private Date interestPostedTillDate; + + @Transient + private BigDecimal runningBalanceOnInterestPostingTillDate = BigDecimal.ZERO; + SavingsAccountSummary() { // } @@ -102,11 +112,130 @@ public final class SavingsAccountSummary { this.totalOverdraftInterestDerived = wrapper.calculateTotalOverdraftInterest(currency, transactions); this.totalWithholdTax = wrapper.calculateTotalWithholdTaxWithdrawal(currency, transactions); + // boolean isUpdated = false; + updateRunningBalanceAndPivotDate(false, transactions, null, null, currency); + this.accountBalance = Money.of(currency, this.totalDeposits).plus(this.totalInterestPosted).minus(this.totalWithdrawals) .minus(this.totalWithdrawalFees).minus(this.totalAnnualFees).minus(this.totalFeeCharge).minus(this.totalPenaltyCharge) .minus(totalOverdraftInterestDerived).minus(totalWithholdTax).getAmount(); } + public void updateSummaryWithPivotConfig(final MonetaryCurrency currency, final SavingsAccountTransactionSummaryWrapper wrapper, + final SavingsAccountTransaction transaction, final List savingsAccountTransactions) { + + if (transaction != null) { + Money transactionAmount = Money.of(currency, transaction.getAmount()); + switch (SavingsAccountTransactionType.fromInt(transaction.getTypeOf())) { + case DEPOSIT: + if (transaction.isDepositAndNotReversed() || transaction.isDividendPayoutAndNotReversed()) { + this.totalDeposits = Money.of(currency, this.totalDeposits).plus(transactionAmount).getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).plus(transactionAmount).getAmount(); + } + break; + case WITHDRAWAL: + if (transaction.isWithdrawal() && transaction.isNotReversed()) { + this.totalWithdrawals = Money.of(currency, this.totalWithdrawals).plus(transactionAmount).getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + case WITHDRAWAL_FEE: + if (transaction.isWithdrawalFeeAndNotReversed() && transaction.isNotReversed()) { + this.totalWithdrawalFees = Money.of(currency, this.totalWithdrawalFees).plus(transactionAmount).getAmount(); + this.totalFeeCharge = Money.of(currency, this.totalFeeCharge).plus(transactionAmount).getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + case ANNUAL_FEE: + if (transaction.isAnnualFeeAndNotReversed() && transaction.isNotReversed()) { + this.totalAnnualFees = Money.of(currency, this.totalAnnualFees).plus(transactionAmount).getAmount(); + this.totalFeeCharge = Money.of(currency, this.totalFeeCharge).plus(transactionAmount).getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + case WAIVE_CHARGES: + if (transaction.isWaiveFeeChargeAndNotReversed()) { + this.totalFeeChargesWaived = Money.of(currency, this.totalFeeChargesWaived).plus(transactionAmount.getAmount()) + .getAmount(); + } else if (transaction.isWaivePenaltyChargeAndNotReversed()) { + this.totalPenaltyChargesWaived = Money.of(currency, this.totalPenaltyChargesWaived) + .plus(transactionAmount.getAmount()).getAmount(); + } + break; + case PAY_CHARGE: + if (transaction.isFeeChargeAndNotReversed()) { + this.totalFeeCharge = Money.of(currency, this.totalFeeCharge).plus(transactionAmount).getAmount(); + } else if (transaction.isPenaltyChargeAndNotReversed()) { + this.totalPenaltyCharge = Money.of(currency, this.totalPenaltyCharge).plus(transactionAmount).getAmount(); + } + if (transaction.isFeeChargeAndNotReversed() || transaction.isPenaltyChargeAndNotReversed()) { + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + case OVERDRAFT_INTEREST: + if (transaction.isOverdraftInterestAndNotReversed()) { + this.totalOverdraftInterestDerived = Money.of(currency, this.totalOverdraftInterestDerived).plus(transactionAmount) + .getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + case WITHHOLD_TAX: + if (transaction.isWithHoldTaxAndNotReversed()) { + this.totalWithholdTax = Money.of(currency, this.totalWithholdTax).plus(transactionAmount).getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + default: + break; + } + } else { + // boolean isUpdated = false; + Money interestTotal = Money.of(currency, this.totalInterestPosted); + Money withHoldTaxTotal = Money.of(currency, this.totalWithholdTax); + + final HashMap map = updateRunningBalanceAndPivotDate(true, savingsAccountTransactions, interestTotal, + withHoldTaxTotal, currency); + interestTotal = map.get("interestTotal"); + withHoldTaxTotal = map.get("withHoldTax"); + this.totalInterestPosted = interestTotal.getAmountDefaultedToNullIfZero(); + this.totalWithholdTax = withHoldTaxTotal.getAmountDefaultedToNullIfZero(); + this.accountBalance = Money.of(currency, this.accountBalance).plus(this.totalInterestPosted).minus(this.totalWithholdTax) + .getAmount(); + } + } + + @SuppressWarnings("unchecked") + private HashMap updateRunningBalanceAndPivotDate(final boolean backdatedTxnsAllowedTill, + final List savingsAccountTransactions, Money interestTotal, Money withHoldTaxTotal, + MonetaryCurrency currency) { + boolean isUpdated = false; + HashMap map = new HashMap<>(); + for (int i = savingsAccountTransactions.size() - 1; i >= 0; i--) { + final SavingsAccountTransaction savingsAccountTransaction = savingsAccountTransactions.get(i); + if (savingsAccountTransaction.isInterestPostingAndNotReversed() && savingsAccountTransaction.isNotReversed() && !isUpdated) { + setRunningBalanceOnPivotDate(savingsAccountTransaction.getRunningBalance(currency).getAmount()); + setInterestPostedTillDate(savingsAccountTransaction.getLastTransactionDate()); + isUpdated = true; + if (!backdatedTxnsAllowedTill) { + break; + } + } + if (backdatedTxnsAllowedTill) { + if (savingsAccountTransaction.isInterestPostingAndNotReversed() && savingsAccountTransaction.isNotReversed()) { + interestTotal = interestTotal.plus(savingsAccountTransaction.getAmount(currency)); + } + + if (savingsAccountTransaction.isWithHoldTaxAndNotReversed()) { + withHoldTaxTotal = withHoldTaxTotal.plus(savingsAccountTransaction.getAmount(currency)); + } + } + } + if (backdatedTxnsAllowedTill) { + map.put("interestTotal", interestTotal); + map.put("withHoldTax", withHoldTaxTotal); + } + return map; + } + public void updateFromInterestPeriodSummaries(final MonetaryCurrency currency, final List allPostingPeriods) { Money totalEarned = Money.zero(currency); @@ -137,4 +266,24 @@ public final class SavingsAccountSummary { return this.totalInterestPosted; } + public Date getLastInterestCalculationDate() { + return this.lastInterestCalculationDate; + } + + public void setInterestPostedTillDate(final Date date) { + this.interestPostedTillDate = date; + } + + public Date getInterestPostedTillDate() { + return this.interestPostedTillDate; + } + + public void setRunningBalanceOnPivotDate(final BigDecimal runningBalanceOnPivotDate) { + this.runningBalanceOnInterestPostingTillDate = runningBalanceOnPivotDate; + } + + public BigDecimal getRunningBalanceOnPivotDate() { + return this.runningBalanceOnInterestPostingTillDate; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java index edb99fabb..c9db05ff0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java @@ -445,6 +445,10 @@ public final class SavingsAccountTransaction extends AbstractPersistableCustom { return LocalDate.ofInstant(this.dateOf.toInstant(), DateUtils.getDateTimeZoneOfTenant()); } + public Date getLastTransactionDate() { + return this.dateOf; + } + public LocalDate getEndOfBalanceLocalDate() { return balanceEndDate == null ? null : LocalDate.ofInstant(balanceEndDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()); } 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 b2fb567fc..cdb1d9337 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 @@ -18,8 +18,12 @@ */ package org.apache.fineract.portfolio.savings.domain; +import java.util.Date; +import java.util.List; +import javax.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -30,4 +34,12 @@ public interface SavingsAccountTransactionRepository SavingsAccountTransaction findOneByIdAndSavingsAccountId(@Param("transactionId") Long transactionId, @Param("savingsId") Long savingsId); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select st from SavingsAccountTransaction st where st.savingsAccount = :savingsAccount and st.dateOf >= :date order by st.dateOf,st.createdDate,st.id") + List findTransactionsAfterPivotDate(@Param("savingsAccount") SavingsAccount savingsAccount, + @Param("date") Date date); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + List findBySavingsAccount(@Param("savingsAccount") SavingsAccount savingsAccount); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java index 51f1c639a..03ffb8edb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java @@ -61,6 +61,17 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalInterestPosted(final MonetaryCurrency currency, final BigDecimal currentInterestPosted, + final List savingsAccountTransactions) { + Money total = Money.of(currency, currentInterestPosted); + for (final SavingsAccountTransaction transaction : savingsAccountTransactions) { + if (transaction.isInterestPostingAndNotReversed() && transaction.isNotReversed()) { + total = total.plus(transaction.getAmount(currency)); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalWithdrawalFees(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { @@ -122,6 +133,17 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalOverdraftInterest(MonetaryCurrency currency, BigDecimal overdraftPosted, + List transactions) { + Money total = Money.of(currency, overdraftPosted); + for (final SavingsAccountTransaction transaction : transactions) { + if (transaction.isOverdraftInterestAndNotReversed()) { + total = total.plus(transaction.getAmount(currency)); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalOverdraftInterest(MonetaryCurrency currency, List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsHelper.java index 14930e103..48c83d7a4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsHelper.java @@ -23,6 +23,7 @@ import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.List; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -176,4 +177,9 @@ public final class SavingsHelper { public Collection fetchPostInterestTransactionIds(Long accountId) { return this.accountTransfersReadPlatformService.fetchPostInterestTransactionIds(accountId); } + + public Collection fetchPostInterestTransactionIds(Long accountId, Date pivotDate) { + return this.accountTransfersReadPlatformService.fetchPostInterestTransactionIdsWithPivotDate(accountId, pivotDate); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/exception/TransactionBeforePivotDateNotAllowed.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/exception/TransactionBeforePivotDateNotAllowed.java new file mode 100644 index 000000000..c3d9a2d0f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/exception/TransactionBeforePivotDateNotAllowed.java @@ -0,0 +1,31 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.portfolio.savings.exception; + +import java.time.LocalDate; +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class TransactionBeforePivotDateNotAllowed extends AbstractPlatformDomainRuleException { + + public TransactionBeforePivotDateNotAllowed(final LocalDate date, final LocalDate interestPostingDate) { + super("error.msg.savings.transaction.is.not.allowed", + "Savings Account transaction date " + date + " should be after the last interest posting date " + interestPostingDate); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java index 50356251d..59bafadf3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java @@ -587,6 +587,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead selectFieldsSqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); selectFieldsSqlBuilder.append("sa.total_deposits_derived as totalDeposits, "); selectFieldsSqlBuilder.append("sa.total_withdrawals_derived as totalWithdrawals, "); + selectFieldsSqlBuilder.append("sa.interest_posted_till_date as interestPostedTillDate, "); selectFieldsSqlBuilder.append("sa.total_withdrawal_fees_derived as totalWithdrawalFees, "); selectFieldsSqlBuilder.append("sa.total_annual_fees_derived as totalAnnualFees, "); selectFieldsSqlBuilder.append("sa.total_interest_earned_derived as totalInterestEarned, "); @@ -739,6 +740,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead final BigDecimal totalPenaltyCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalPenaltyCharge"); final BigDecimal totalWithholdTax = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithholdTax"); final BigDecimal totalOverdraftInterestDerived = null; + final LocalDate interestPostedTillDate = JdbcSupport.getLocalDate(rs, "interestPostedTillDate"); final boolean withHoldTax = rs.getBoolean("withHoldTax"); final Long taxGroupId = JdbcSupport.getLong(rs, "taxGroupId"); @@ -751,7 +753,8 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead final BigDecimal availableBalance = null; final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals, totalWithdrawalFees, totalAnnualFees, totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge, - totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, null, null, availableBalance); + totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, null, null, availableBalance, + interestPostedTillDate); return DepositAccountData.instance(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestCompoundingPeriodType, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index 84004eb4a..3de6da11f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -244,14 +244,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo if (activationChargeAmount.isGreaterThanZero()) { payActivationCharge(account, user); } - if (account.isBeforeLastPostingPeriod(account.getActivationLocalDate())) { + if (account.isBeforeLastPostingPeriod(account.getActivationLocalDate(), false)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); } updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); @@ -266,7 +266,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); } account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), - depositAccountOnHoldTransactions); + depositAccountOnHoldTransactions, false); this.savingAccountRepositoryWrapper.saveAndFlush(account); } postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); @@ -295,7 +295,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo for (SavingsAccountCharge savingsAccountCharge : account.charges()) { if (savingsAccountCharge.isSavingsActivation()) { account.payCharge(savingsAccountCharge, savingsAccountCharge.getAmount(account.getCurrency()), - account.getActivationLocalDate(), user); + account.getActivationLocalDate(), user, false); } } } @@ -368,14 +368,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo account.updateOverduePayments(overdueUptoDate); final boolean isInterestTransfer = false; final LocalDate postInterestOnDate = null; - if (account.isBeforeLastPostingPeriod(account.getActivationLocalDate())) { + if (account.isBeforeLastPostingPeriod(account.getActivationLocalDate(), false)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, false); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); } List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { @@ -384,7 +384,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo } account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), - depositAccountOnHoldTransactions); + depositAccountOnHoldTransactions, false); this.savingAccountRepositoryWrapper.saveAndFlush(account); } @@ -527,7 +527,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo boolean isInterestTransfer = false; LocalDate postInterestOnDate = null; account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); this.savingAccountRepositoryWrapper.save(account); @@ -571,7 +571,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo boolean isInterestTransfer = false; LocalDate postInterestOnDate = null; account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, false); this.savingAccountRepositoryWrapper.saveAndFlush(account); postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); @@ -645,12 +645,12 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo LocalDate postInterestOnDate = null; checkClientOrGroupActive(account); if (savingsAccountTransaction.isPostInterestCalculationRequired() - && account.isBeforeLastPostingPeriod(savingsAccountTransaction.transactionLocalDate())) { + && account.isBeforeLastPostingPeriod(savingsAccountTransaction.transactionLocalDate(), false)) { account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, false); } else { account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); } List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { @@ -658,7 +658,8 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); } - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions); + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions, + false); // account.activateAccountBasedOnBalance(); final boolean isPreMatureClosure = false; account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, @@ -745,22 +746,22 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo if (savingsAccountTransaction.isDeposit()) { final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, paymentDetail, savingsAccountTransaction.createdDate(), user, accountType); - transaction = account.deposit(transactionDTO); + transaction = account.deposit(transactionDTO, false); } else { final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, paymentDetail, savingsAccountTransaction.createdDate(), user, accountType); - transaction = account.withdraw(transactionDTO, true); + transaction = account.withdraw(transactionDTO, true, false); } final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction); boolean isInterestTransfer = false; final LocalDate postInterestOnDate = null; - if (account.isBeforeLastPostingPeriod(transactionDate) - || account.isBeforeLastPostingPeriod(savingsAccountTransaction.transactionLocalDate())) { + if (account.isBeforeLastPostingPeriod(transactionDate, false) + || account.isBeforeLastPostingPeriod(savingsAccountTransaction.transactionLocalDate(), false)) { account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, false); } else { account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); } List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { @@ -768,7 +769,8 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); } - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions); + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions, + false); account.activateAccountBasedOnBalance(); if (savingsAccountTransaction.isDeposit()) { @@ -982,7 +984,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final MathContext mc = MathContext.DECIMAL64; boolean isInterestTransfer = false; savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); this.savingsAccountTransactionRepository.save(newTransferTransaction); this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); @@ -1016,7 +1018,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo boolean isInterestTransfer = false; LocalDate postInterestOnDate = null; savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); this.savingsAccountTransactionRepository.save(withdrawtransferTransaction); this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); @@ -1060,7 +1062,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo LocalDate postInterestOnDate = null; final MathContext mc = MathContext.DECIMAL64; savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); this.savingsAccountTransactionRepository.save(acceptTransferTransaction); this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); @@ -1209,18 +1211,18 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final Set existingReversedTransactionIds = new HashSet<>(); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - account.waiveCharge(savingsAccountChargeId, user); + account.waiveCharge(savingsAccountChargeId, user, false); boolean isInterestTransfer = false; LocalDate postInterestOnDate = null; final MathContext mc = MathContext.DECIMAL64; - if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueLocalDate())) { + if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueLocalDate(), false)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, false); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); } List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { @@ -1229,7 +1231,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo } account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction, - depositAccountOnHoldTransactions); + depositAccountOnHoldTransactions, false); this.savingAccountRepositoryWrapper.saveAndFlush(account); @@ -1348,18 +1350,18 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, user); + account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, user, false); boolean isInterestTransfer = false; LocalDate postInterestOnDate = null; final MathContext mc = MathContext.DECIMAL64; - if (account.isBeforeLastPostingPeriod(transactionDate)) { + if (account.isBeforeLastPostingPeriod(transactionDate, false)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, false); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); } List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { @@ -1368,7 +1370,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo } account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(), - depositAccountOnHoldTransactions); + depositAccountOnHoldTransactions, false); this.savingAccountRepositoryWrapper.saveAndFlush(account); @@ -1436,7 +1438,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepositoryWrapper.findOneWithNotFoundDetection(currency); boolean isAccountTransfer = false; final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(applicationCurrency.toData(), - existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); + existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, false); this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 63dd5080e..8c2923dee 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -313,6 +313,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append("sa.withhold_tax as withHoldTax, "); sqlBuilder.append("sa.total_withhold_tax_derived as totalWithholdTax, "); sqlBuilder.append("sa.last_interest_calculation_date as lastInterestCalculationDate, "); + sqlBuilder.append("sa.interest_posted_till_date as interestPostedTillDate, "); sqlBuilder.append("sa.total_savings_amount_on_hold as onHoldAmount, "); sqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName, "); sqlBuilder.append("(select IFNULL(max(sat.transaction_date),sa.activatedon_date) "); @@ -508,6 +509,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final BigDecimal totalOverdraftInterestDerived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalOverdraftInterestDerived"); final BigDecimal totalWithholdTax = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithholdTax"); + final LocalDate interestPostedTillDate = JdbcSupport.getLocalDate(rs, "interestPostedTillDate"); final BigDecimal minBalanceForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "minBalanceForInterestCalculation"); @@ -536,7 +538,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals, totalWithdrawalFees, totalAnnualFees, totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge, totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, interestNotPosted, lastInterestCalculationDate, - availableBalance); + availableBalance, interestPostedTillDate); final boolean withHoldTax = rs.getBoolean("withHoldTax"); final Long taxGroupId = JdbcSupport.getLong(rs, "taxGroupId"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java index bbc7eb98b..b72d60ca5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java @@ -85,7 +85,7 @@ public interface SavingsAccountWritePlatformService { CommandProcessingResult postInterest(JsonCommand command); - void postInterest(SavingsAccount account, boolean postInterestAs, LocalDate transactionDate); + void postInterest(SavingsAccount account, boolean postInterestAs, LocalDate transactionDate, boolean backdatedTxnsAllowedTill); CommandProcessingResult blockAccount(Long savingsId); 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 e43786496..712ed12b2 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 @@ -239,11 +239,14 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi this.savingsAccountTransactionDataValidator.validateActivation(command); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + final Map changes = account.activate(user, command, DateUtils.getLocalDateOfTenant()); entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVING.getName(), @@ -253,11 +256,10 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds); - this.savingAccountRepositoryWrapper.saveAndFlush(account); } - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.SAVINGS_ACTIVATE, constructEntityMap(BusinessEntity.SAVING, account)); @@ -286,17 +288,20 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi if (amountForDeposit.isGreaterThanZero()) { boolean isAccountTransfer = false; this.savingsAccountDomainService.handleDeposit(account, fmt, account.getActivationLocalDate(), amountForDeposit.getAmount(), - null, isAccountTransfer, isRegularTransaction); + null, isAccountTransfer, isRegularTransaction, false); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); } + account.processAccountUponActivation(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, user); List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); } + account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), - depositAccountOnHoldTransactions); + depositAccountOnHoldTransactions, false); } @Transactional @@ -330,25 +335,30 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi this.savingsAccountTransactionDataValidator.validate(command); boolean isGsim = false; - if (this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsId).getGsim() != null) { + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + + if (account.getGsim() != null) { isGsim = true; LOG.info("is gsim"); } - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); checkClientOrGroupActive(account); + final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); + final Map changes = new LinkedHashMap<>(); final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); boolean isAccountTransfer = false; boolean isRegularTransaction = true; final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, - transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction); + transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); if (isGsim && (deposit.getId() != null)) { @@ -394,9 +404,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi this.savingsAccountTransactionDataValidator.validate(command); boolean isGsim = false; - if (this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsId).getGsim() != null) { - isGsim = true; - } final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); @@ -407,8 +414,17 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final Map changes = new LinkedHashMap<>(); final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + + if (account.getGsim() != null) { + isGsim = true; + } checkClientOrGroupActive(account); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); + final boolean isAccountTransfer = false; final boolean isRegularTransaction = true; final boolean isApplyWithdrawFee = true; @@ -417,7 +433,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance); final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(account, fmt, transactionDate, - transactionAmount, paymentDetail, transactionBooleanValues); + transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); if (isGsim && (withdrawal.getId() != null)) { GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).get(); @@ -456,7 +472,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant()); while (todaysDate.isAfter(savingsAccountCharge.getDueLocalDate())) { - this.payCharge(savingsAccountCharge, savingsAccountCharge.getDueLocalDate(), savingsAccountCharge.amount(), fmt, user); + this.payCharge(savingsAccountCharge, savingsAccountCharge.getDueLocalDate(), savingsAccountCharge.amount(), fmt, user, false); } return new CommandProcessingResultBuilder() // @@ -476,14 +492,21 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi .isSavingsInterestPostingAtCurrentPeriodEnd(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); checkClientOrGroupActive(account); + final LocalDate today = DateUtils.getLocalDateOfTenant(); final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); boolean isInterestTransfer = false; final LocalDate postInterestOnDate = null; + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } this.savingAccountRepositoryWrapper.save(account); @@ -502,8 +525,13 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi Long savingsId = command.getSavingsId(); final boolean postInterestAs = command.booleanPrimitiveValueOfParameterNamed("isPostInterestAsOn"); final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); checkClientOrGroupActive(account); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); + if (postInterestAs == true) { if (transactionDate == null) { @@ -513,7 +541,14 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi if (transactionDate.isBefore(account.accountSubmittedOrActivationDate())) { throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.ACTIVATION_DATE); } - List savingTransactions = account.getTransactions(); + + List savingTransactions = null; + if (backdatedTxnsAllowedTill) { + savingTransactions = account.getSavingsAccountTransactionsWithPivotConfig(); + } else { + savingTransactions = account.getTransactions(); + } + for (SavingsAccountTransaction savingTransaction : savingTransactions) { if (Date.from(transactionDate.atStartOfDay(ZoneId.systemDefault()).toInstant()).before(savingTransaction.getDateOf())) { throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.LAST_TRANSACTION_DATE); @@ -526,7 +561,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi } } - postInterest(account, postInterestAs, transactionDate); + postInterest(account, postInterestAs, transactionDate, backdatedTxnsAllowedTill); this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.SAVINGS_POST_INTEREST, constructEntityMap(BusinessEntity.SAVING, account)); @@ -541,7 +576,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi @Transactional @Override - public void postInterest(final SavingsAccount account, final boolean postInterestAs, final LocalDate transactionDate) { + public void postInterest(final SavingsAccount account, final boolean postInterestAs, final LocalDate transactionDate, + final boolean backdatedTxnsAllowedTill) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -550,7 +586,13 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi || (account.allowOverdraft() && account.getNominalAnnualInterestRateOverdraft().compareTo(BigDecimal.ZERO) > 0)) { final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + if (backdatedTxnsAllowedTill) { + updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + final LocalDate today = DateUtils.getLocalDateOfTenant(); final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); boolean isInterestTransfer = false; @@ -558,19 +600,26 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi if (postInterestAs) { postInterestOnDate = transactionDate; } + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); - // for generating transaction id's - List transactions = account.getTransactions(); - for (SavingsAccountTransaction accountTransaction : transactions) { - if (accountTransaction.getId() == null) { - this.savingsAccountTransactionRepository.save(accountTransaction); + postInterestOnDate, backdatedTxnsAllowedTill); + + if (!backdatedTxnsAllowedTill) { + List transactions = account.getTransactions(); + for (SavingsAccountTransaction accountTransaction : transactions) { + if (accountTransaction.getId() == null) { + this.savingsAccountTransactionRepository.save(accountTransaction); + } } } + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); } } @@ -582,7 +631,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi .isSavingsInterestPostingAtCurrentPeriodEnd(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); @@ -593,6 +642,9 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); } + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(savingsAccountTransaction.getTransactionLocalDate(), + account); + if (!allowAccountTransferModification && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", @@ -626,22 +678,23 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi LocalDate postInterestOnDate = null; checkClientOrGroupActive(account); if (savingsAccountTransaction.isPostInterestCalculationRequired() - && account.isBeforeLastPostingPeriod(savingsAccountTransaction.transactionLocalDate())) { + && account.isBeforeLastPostingPeriod(savingsAccountTransaction.transactionLocalDate(), false)) { account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, false); } else { account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); } List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); } - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions); + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions, + false); account.activateAccountBasedOnBalance(); this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); return new CommandProcessingResultBuilder() // .withEntityId(savingsId) // .withOfficeId(account.officeId()) // @@ -681,7 +734,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final LocalDate today = DateUtils.getLocalDateOfTenant(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + if (account.isNotActive()) { throwValidationForActiveStatus(SavingsApiConstants.adjustTransactionAction); } @@ -691,6 +745,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi } final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); final Locale locale = command.extractLocale(); @@ -716,29 +771,32 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, paymentDetail, savingsAccountTransaction.createdDate(), user, accountType); if (savingsAccountTransaction.isDeposit()) { - transaction = account.deposit(transactionDTO); + transaction = account.deposit(transactionDTO, false); } else { - transaction = account.withdraw(transactionDTO, true); + transaction = account.withdraw(transactionDTO, true, false); } final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction); final LocalDate postInterestOnDate = null; - if (account.isBeforeLastPostingPeriod(transactionDate) - || account.isBeforeLastPostingPeriod(savingsAccountTransaction.transactionLocalDate())) { + if (account.isBeforeLastPostingPeriod(transactionDate, false) + || account.isBeforeLastPostingPeriod(savingsAccountTransaction.transactionLocalDate(), false)) { account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, false); } else { account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); } List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); } - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions); + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions, + false); account.activateAccountBasedOnBalance(); + this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); return new CommandProcessingResultBuilder() // .withEntityId(newtransactionId) // .withOfficeId(account.officeId()) // @@ -802,7 +860,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi public CommandProcessingResult close(final Long savingsId, final JsonCommand command) { final AppUser user = this.context.authenticatedUser(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); this.savingsAccountTransactionDataValidator.validateClosing(command, account); final boolean isLinkedWithAnyActiveLoan = this.accountAssociationsReadPlatformService.isLinkedWithAnyActiveAccount(savingsId); @@ -854,7 +912,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance); this.savingsAccountDomainService.handleWithdrawal(account, fmt, closedDate, transactionAmount, paymentDetail, - transactionBooleanValues); + transactionBooleanValues, false); } @@ -909,12 +967,12 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi boolean isInterestTransfer = false; LocalDate postInterestOnDate = null; savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); this.savingsAccountTransactionRepository.save(newTransferTransaction); this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); return newTransferTransaction; } @@ -940,12 +998,12 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi boolean isInterestTransfer = false; LocalDate postInterestOnDate = null; savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); this.savingsAccountTransactionRepository.save(withdrawtransferTransaction); this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); return withdrawtransferTransaction; } @@ -982,12 +1040,12 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final MathContext mc = MathContext.DECIMAL64; LocalDate postInterestOnDate = null; savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, false); this.savingsAccountTransactionRepository.save(acceptTransferTransaction); this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); return acceptTransferTransaction; } @@ -1004,7 +1062,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final Long savingsAccountId = command.getSavingsId(); this.savingsAccountChargeDataValidator.validateAdd(command.json()); - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId); + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); checkClientOrGroupActive(savingsAccount); final Locale locale = command.extractLocale(); @@ -1073,7 +1131,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi // SavingsAccount Charge entity final Long savingsChargeId = command.entityId(); - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId); + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); checkClientOrGroupActive(savingsAccount); final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository.findOneWithNotFoundDetection(savingsChargeId, @@ -1132,25 +1190,35 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi // Get Savings account from savings charge final SavingsAccount account = savingsAccountCharge.savingsAccount(); - this.savingAccountAssembler.assignSavingAccountHelpers(account); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + + this.savingAccountAssembler.loadTransactionsToSavingsAccount(account, backdatedTxnsAllowedTill); final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - account.waiveCharge(savingsAccountChargeId, user); + if (backdatedTxnsAllowedTill) { + updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + + account.waiveCharge(savingsAccountChargeId, user, backdatedTxnsAllowedTill); + boolean isInterestTransfer = false; LocalDate postInterestOnDate = null; final MathContext mc = MathContext.DECIMAL64; - if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueLocalDate())) { + if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueLocalDate(), backdatedTxnsAllowedTill)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, backdatedTxnsAllowedTill); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); } + List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository @@ -1158,11 +1226,15 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi } account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction, - depositAccountOnHoldTransactions); + depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); return new CommandProcessingResultBuilder() // .withEntityId(savingsAccountChargeId) // .withOfficeId(account.officeId()) // @@ -1178,7 +1250,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi @SuppressWarnings("unused") final JsonCommand command) { this.context.authenticatedUser(); - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId); + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); checkClientOrGroupActive(savingsAccount); final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); @@ -1232,7 +1304,10 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi } } - SavingsAccountTransaction chargeTransaction = this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt, user); + final boolean backdatedTxnsAllowedTill = false; + + SavingsAccountTransaction chargeTransaction = this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt, user, + backdatedTxnsAllowedTill); final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { @@ -1263,13 +1338,13 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant()); while (transactionDate.isAfter(savingsAccountCharge.getDueLocalDate()) && savingsAccountCharge.isNotFullyPaid()) { - payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt, user); + payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt, user, false); } } @Transactional private SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, - final BigDecimal amountPaid, final DateTimeFormatter formatter, final AppUser user) { + final BigDecimal amountPaid, final DateTimeFormatter formatter, final AppUser user, final boolean backdatedTxnsAllowedTill) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -1281,18 +1356,19 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - SavingsAccountTransaction chargeTransaction = account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, user); + SavingsAccountTransaction chargeTransaction = account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, user, + backdatedTxnsAllowedTill); boolean isInterestTransfer = false; LocalDate postInterestOnDate = null; final MathContext mc = MathContext.DECIMAL64; - if (account.isBeforeLastPostingPeriod(transactionDate)) { + if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate); + postInterestOnDate, isInterestTransfer); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate); + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill); } List depositAccountOnHoldTransactions = null; if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { @@ -1301,13 +1377,13 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi } account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(), - depositAccountOnHoldTransactions); + depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); saveTransactionToGenerateTransactionId(chargeTransaction); this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); return chargeTransaction; } @@ -1318,14 +1394,20 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); } + private void updateSavingsTransactionsDetails(SavingsAccount account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); + existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); + } + private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds) { + final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { final MonetaryCurrency currency = savingsAccount.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepositoryWrapper.findOneWithNotFoundDetection(currency); boolean isAccountTransfer = false; final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(applicationCurrency.toData(), - existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); + existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); } @@ -1502,19 +1584,19 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi @Override @Transactional public void setSubStatusInactive(Long savingsId) { - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - account.setSubStatusInactive(appuserRepository.fetchSystemUser()); + account.setSubStatusInactive(appuserRepository.fetchSystemUser(), false); this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); } @Override @Transactional public void setSubStatusDormant(Long savingsId) { - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); account.setSubStatusDormant(); this.savingAccountRepositoryWrapper.save(account); } @@ -1522,13 +1604,13 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi @Override @Transactional public void escheat(Long savingsId) { - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); account.escheat(appuserRepository.fetchSystemUser()); this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); } private AppUser getAppUserIfPresent() { @@ -1573,7 +1655,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi this.context.authenticatedUser(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(account); final Map changes = account.block(); @@ -1589,7 +1671,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi public CommandProcessingResult unblockAccount(final Long savingsId) { this.context.authenticatedUser(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(account); final Map changes = account.unblock(); @@ -1606,13 +1688,23 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi public CommandProcessingResult holdAmount(final Long savingsId, final JsonCommand command) { final AppUser submittedBy = this.context.authenticatedUser(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + checkClientOrGroupActive(account); SavingsAccountTransaction transacton = this.savingsAccountTransactionDataValidator.validateHoldAndAssembleForm(command.json(), - account, submittedBy); + account, submittedBy, backdatedTxnsAllowedTill); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transacton.getTransactionLocalDate(), account); this.savingsAccountTransactionRepository.save(transacton); + + if (backdatedTxnsAllowedTill) { + // Check again whether transactions are modified + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + this.savingAccountRepositoryWrapper.saveAndFlush(account); return new CommandProcessingResultBuilder().withEntityId(transacton.getId()).withOfficeId(account.officeId()) @@ -1629,12 +1721,21 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final SavingsAccountTransaction transaction = this.savingsAccountTransactionDataValidator .validateReleaseAmountAndAssembleForm(holdTransaction, submittedBy); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); checkClientOrGroupActive(account); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionLocalDate(), account); account.releaseOnHoldAmount(transaction.getAmount()); this.savingsAccountTransactionRepository.save(transaction); holdTransaction.updateReleaseId(transaction.getId()); + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + this.savingAccountRepositoryWrapper.save(account); return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId()) @@ -1645,7 +1746,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi public CommandProcessingResult blockCredits(final Long savingsId) { this.context.authenticatedUser(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(account); final Map changes = account.blockCredits(account.getSubStatus()); @@ -1661,7 +1762,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi public CommandProcessingResult unblockCredits(final Long savingsId) { this.context.authenticatedUser(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(account); final Map changes = account.unblockCredits(); @@ -1677,7 +1778,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi public CommandProcessingResult blockDebits(final Long savingsId) { this.context.authenticatedUser(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(account); final Map changes = account.blockDebits(account.getSubStatus()); @@ -1693,7 +1794,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi public CommandProcessingResult unblockDebits(final Long savingsId) { this.context.authenticatedUser(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(account); final Map changes = account.unblockDebits(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index 2265348dd..3c0c3349b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -359,7 +359,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl final Map changes = new LinkedHashMap<>(20); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(account); account.modifyApplication(command, changes); account.validateNewApplicationState(DateUtils.getLocalDateOfTenant(), SAVINGS_ACCOUNT_RESOURCE_NAME); @@ -451,7 +451,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl @Override public CommandProcessingResult deleteApplication(final Long savingsId) { - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(account); if (account.isNotSubmittedAndPendingApproval()) { @@ -515,7 +515,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl this.savingsAccountApplicationTransitionApiJsonValidator.validateApproval(command.json()); - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(savingsAccount); entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVING.getName(), @@ -581,7 +581,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl this.savingsAccountApplicationTransitionApiJsonValidator.validateForUndo(command.json()); - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(savingsAccount); final Map changes = savingsAccount.undoApplicationApproval(); @@ -639,7 +639,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl this.savingsAccountApplicationTransitionApiJsonValidator.validateRejection(command.json()); - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(savingsAccount); entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVING.getName(), @@ -677,7 +677,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl this.savingsAccountApplicationTransitionApiJsonValidator.validateApplicantWithdrawal(command.json()); - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId); + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId, false); checkClientOrGroupActive(savingsAccount); entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVING.getName(), @@ -752,7 +752,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl generateAccountNumber(account); // post journal entries for activation charges - this.savingsAccountDomainService.postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + this.savingsAccountDomainService.postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); return new CommandProcessingResultBuilder() // .withSavingsId(account.getId()) // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java index 6dcbdae70..f5dde2555 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; @@ -54,6 +55,7 @@ public class SavingsSchedularInterestPoster implements Callable { private SavingsAccountRepositoryWrapper savingsAccountRepository; private SavingsAccountAssembler savingAccountAssembler; private FineractPlatformTenant tenant; + private ConfigurationDomainService configurationDomainService; public void setSavingsIds(Collection savingsIds) { this.savingsIds = savingsIds; @@ -82,7 +84,7 @@ public class SavingsSchedularInterestPoster implements Callable { ThreadLocalContextUtil.setTenant(tenant); Integer maxNumberOfRetries = tenant.getConnection().getMaxRetriesOnDeadlock(); Integer maxIntervalBetweenRetries = tenant.getConnection().getMaxIntervalBetweenRetries(); - + final boolean backdatedTxnsAllowedTill = this.configurationDomainService.retrievePivotDateConfig(); int i = 0; if (!savingsIds.isEmpty()) { List errors = new ArrayList<>(); @@ -95,7 +97,8 @@ public class SavingsSchedularInterestPoster implements Callable { this.savingAccountAssembler.assignSavingAccountHelpers(savingsAccount); boolean postInterestAsOn = false; LocalDate transactionDate = null; - this.savingsAccountWritePlatformService.postInterest(savingsAccount, postInterestAsOn, transactionDate); + this.savingsAccountWritePlatformService.postInterest(savingsAccount, postInterestAsOn, transactionDate, + backdatedTxnsAllowedTill); numberOfRetries = maxNumberOfRetries + 1; } catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountSchedularServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountSchedularServiceImpl.java index 8c0f07918..82b905c47 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountSchedularServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountSchedularServiceImpl.java @@ -50,9 +50,9 @@ public class ShareAccountSchedularServiceImpl implements ShareAccountSchedularSe public void postDividend(final Long dividendDetailId, final Long savingsId) { ShareAccountDividendDetails shareAccountDividendDetails = this.shareAccountDividendRepository.findById(dividendDetailId).get(); - final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsId); + final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsId, false); SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountDomainService.handleDividendPayout(savingsAccount, - DateUtils.getLocalDateOfTenant(), shareAccountDividendDetails.getAmount()); + DateUtils.getLocalDateOfTenant(), shareAccountDividendDetails.getAmount(), false); shareAccountDividendDetails.update(ShareAccountDividendStatusType.POSTED.getValue(), savingsAccountTransaction.getId()); this.shareAccountDividendRepository.save(shareAccountDividendDetails); } diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V376__savings_last_transaction_date.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V376__savings_last_transaction_date.sql new file mode 100644 index 000000000..63dc67f44 --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V376__savings_last_transaction_date.sql @@ -0,0 +1,24 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. +-- + +ALTER TABLE m_savings_account ADD COLUMN interest_posted_till_date DATE NULL DEFAULT NULL; + +insert into c_configuration(name, enabled, value, description) values('allow-backdated-transaction-before-interest-posting', 1, 0, 'Avoid retrieving all transactions in a savings account'); + +insert into c_configuration(name, enabled, value, description) values('allow-backdated-transaction-before-interest-posting-date-for-days', 0, 0, 'One time configuration to relax the backdated transactions'); 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 cb843e44d..ed26486d8 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 @@ -42,6 +42,7 @@ import java.util.Locale; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.CommonConstants; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; import org.apache.fineract.integrationtests.common.PaymentTypeHelper; import org.apache.fineract.integrationtests.common.SchedulerJobHelper; import org.apache.fineract.integrationtests.common.TaxComponentHelper; @@ -128,6 +129,115 @@ public class ClientSavingsIntegrationTest { // verifySavingsInterest(savingsInterest); } + @Test + public void testSavingsLastTransactionAndRunningBalanceUpdate() { + this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + // Assertions.assertNotNull(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, MINIMUM_OPENING_BALANCE, + 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 HashMap summaryBefore = this.savingsAccountHelper.getSavingsSummary(savingsId); + this.savingsAccountHelper.calculateInterestForSavings(savingsId); + HashMap summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(summaryBefore, summary); + + this.savingsAccountHelper.postInterestForSavings(savingsId); + + HashMap summaryAfterPosting = this.savingsAccountHelper.getSavingsSummary(savingsId); + + Assertions.assertNotNull(summaryAfterPosting.get("interestPostedTillDate")); + } + + @SuppressWarnings("unchecked") + @Test + public void testSavingsBackedDatedTransactionsNotAllowed() { + this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); + + final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); + // Assertions.assertNotNull(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, MINIMUM_OPENING_BALANCE, + 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 HashMap summaryBefore = this.savingsAccountHelper.getSavingsSummary(savingsId); + this.savingsAccountHelper.calculateInterestForSavings(savingsId); + HashMap summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + assertEquals(summaryBefore, summary); + + this.savingsAccountHelper.postInterestForSavings(savingsId); + + HashMap summaryAfterPosting = this.savingsAccountHelper.getSavingsSummary(savingsId); + + Assertions.assertNotNull(summaryAfterPosting.get("interestPostedTillDate")); + + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "38", false); + + final ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(403).build(); + final SavingsAccountHelper validationErrorHelper = new SavingsAccountHelper(this.requestSpec, errorResponse); + + SimpleDateFormat sdf = new SimpleDateFormat(CommonConstants.DATE_FORMAT, Locale.US); + Calendar cal = Calendar.getInstance(); + List dates = (List) summaryAfterPosting.get("interestPostedTillDate"); + cal.set(Calendar.YEAR, (Integer) dates.get(0)); + cal.set(Calendar.MONTH, (Integer) dates.get(1) - 1); + cal.set(Calendar.DAY_OF_MONTH, (Integer) dates.get(2) - 1); + final String depositDate = sdf.format(cal.getTime()); + LOG.info("-------------------- BackDated Transaction -----------------------------"); + List error = (List) validationErrorHelper.depositToSavingsAccount(savingsId, "3000", depositDate, + CommonConstants.RESPONSE_ERROR); + + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "38", true); + + // LOG.info(savingsAccountErrorData.get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE).toString()); + assertEquals("error.msg.savings.transaction.is.not.allowed", error.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); + + } + @Test public void testSavingsAccountWithMinBalanceForInterestCalculation() { this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec); @@ -166,6 +276,10 @@ public class ClientSavingsIntegrationTest { this.savingsAccountHelper.postInterestForSavings(savingsId); summary = this.savingsAccountHelper.getSavingsSummary(savingsId); + + // These two fields are getting updated after an interest posting + summary.remove("interestPostedTillDate"); + assertEquals(summaryBefore, summary); final Object savingsInterest = this.savingsAccountHelper.getSavingsInterest(savingsId); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java index c295ce5a4..04ecd2772 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java @@ -76,6 +76,7 @@ public class GlobalConfigurationHelper { // the future, it needs to be reset here. final Integer configDefaultId = (Integer) configDefault.get("id"); final Integer configDefaultValue = (Integer) configDefault.get("value"); + updateValueForGlobalConfiguration(requestSpec, responseSpec, configDefaultId.toString(), configDefaultValue.toString()); updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, configDefaultId.toString(), (Boolean) configDefault.get("enabled")); @@ -88,9 +89,9 @@ public class GlobalConfigurationHelper { ArrayList expectedGlobalConfigurations = getAllDefaultGlobalConfigurations(); ArrayList actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec); - // There are currently 32 global configurations. - Assertions.assertEquals(33, expectedGlobalConfigurations.size()); - Assertions.assertEquals(33, actualGlobalConfigurations.size()); + // There are currently 35 global configurations. + Assertions.assertEquals(35, expectedGlobalConfigurations.size()); + Assertions.assertEquals(35, actualGlobalConfigurations.size()); for (int i = 0; i < expectedGlobalConfigurations.size(); i++) { @@ -389,6 +390,23 @@ public class GlobalConfigurationHelper { isNextDayFixedDepositInterestTransferEnabledForPeriodEnd.put("trapDoor", false); defaults.add(isNextDayFixedDepositInterestTransferEnabledForPeriodEnd); + HashMap isAllowedBackDatedTransactionsBeforeInterestPostingDate = new HashMap<>(); + isAllowedBackDatedTransactionsBeforeInterestPostingDate.put("id", 38); + isAllowedBackDatedTransactionsBeforeInterestPostingDate.put("name", "allow-backdated-transaction-before-interest-posting"); + isAllowedBackDatedTransactionsBeforeInterestPostingDate.put("value", 0); + isAllowedBackDatedTransactionsBeforeInterestPostingDate.put("enabled", true); + isAllowedBackDatedTransactionsBeforeInterestPostingDate.put("trapDoor", false); + defaults.add(isAllowedBackDatedTransactionsBeforeInterestPostingDate); + + HashMap isAllowedBackDatedTransactionsBeforeInterestPostingDateForDays = new HashMap<>(); + isAllowedBackDatedTransactionsBeforeInterestPostingDateForDays.put("id", 39); + isAllowedBackDatedTransactionsBeforeInterestPostingDateForDays.put("name", + "allow-backdated-transaction-before-interest-posting-date-for-days"); + isAllowedBackDatedTransactionsBeforeInterestPostingDateForDays.put("value", 0); + isAllowedBackDatedTransactionsBeforeInterestPostingDateForDays.put("enabled", false); + isAllowedBackDatedTransactionsBeforeInterestPostingDateForDays.put("trapDoor", false); + defaults.add(isAllowedBackDatedTransactionsBeforeInterestPostingDateForDays); + return defaults; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java index d2ee39677..3e929a696 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java @@ -147,8 +147,8 @@ public class SchedulerJobHelper { * @author Michael Vorburger.ch */ public void executeAndAwaitJob(String jobName) { - final Duration timeout = Duration.ofMinutes(3); - final Duration pause = Duration.ofSeconds(1); + final Duration timeout = Duration.ofMinutes(4); + final Duration pause = Duration.ofSeconds(2); DateTimeFormatter df = DateTimeFormatter.ISO_INSTANT; // FINERACT-926 Instant beforeExecuteTime = now().truncatedTo(ChronoUnit.SECONDS);