diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/accrual/handler/ExecutePeriodicAccrualForSavingsCommandHandler.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/accrual/handler/ExecutePeriodicAccrualForSavingsCommandHandler.java new file mode 100644 index 00000000000..ecc64eeb6d6 --- /dev/null +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/accrual/handler/ExecutePeriodicAccrualForSavingsCommandHandler.java @@ -0,0 +1,42 @@ +/** + * 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.accounting.accrual.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.accrual.service.AccrualAccountingWritePlatformService; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "PERIODICACCRUALACCOUNTINGFORSAVINGS", action = "EXECUTE") +@RequiredArgsConstructor +public class ExecutePeriodicAccrualForSavingsCommandHandler implements NewCommandSourceHandler { + + private final AccrualAccountingWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.executeLoansPeriodicAccrual(command); + } +} diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java index 511ce5e801b..6cce7290efc 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java @@ -169,7 +169,8 @@ public void validateForSavingsProductCreate(final String json, DepositAccountTyp Locale.getDefault()); baseDataValidator.reset().parameter(accountingRuleParamName).value(accountingRuleType).notNull().inMinMaxRange(1, 3); - if (AccountingValidations.isCashBasedAccounting(accountingRuleType)) { + if (AccountingValidations.isCashBasedAccounting(accountingRuleType) + || AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) { final Long savingsControlAccountId = this.fromApiJsonHelper .extractLongNamed(SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), element); @@ -225,7 +226,6 @@ public void validateForSavingsProductCreate(final String json, DepositAccountTyp baseDataValidator.reset().parameter(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue()).value(writtenOff).notNull() .integerGreaterThanZero(); } - } // Periodic Accrual Accounting aditional GL Accounts diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java index 1b11ae5ec43..2c26ae4530f 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java @@ -100,6 +100,7 @@ public void mergeProductToAccountMappingChanges(final JsonElement element, final optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditInterestAccountId"); optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditFeesAccountId"); optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditPenaltyAccountId"); + optionalProductToGLAccountMappingEntries.add("interestReceivableAccountId"); optionalProductToGLAccountMappingEntries.add(LoanProductAccountingParams.DEFERRED_INCOME_LIABILITY.getValue()); optionalProductToGLAccountMappingEntries.add(LoanProductAccountingParams.INCOME_FROM_CAPITALIZATION.getValue()); optionalProductToGLAccountMappingEntries.add(LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue()); diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java index 3c250a0b6df..6e2c44c2378 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java @@ -358,6 +358,8 @@ private Map setAccrualPeriodicSavingsProductToGLAccountMaps(fina // Assets if (glAccountForSavings.equals(AccrualAccountsForSavings.SAVINGS_REFERENCE)) { accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_REFERENCE.getValue(), glAccountData); + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.INTEREST_RECEIVABLE)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_RECEIVABLE.getValue(), glAccountData); } else if (glAccountForSavings.equals(AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL)) { accountMappingDetails.put(SavingProductAccountingDataParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), glAccountData); } else if (glAccountForSavings.equals(AccrualAccountsForSavings.FEES_RECEIVABLE)) { diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java index 903ae202546..27cb564a263 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java @@ -263,9 +263,9 @@ public void handleChangesToSavingsProductToGLAccountMappings(final Long savingsP savingsProductId, AccrualAccountsForSavings.FEES_RECEIVABLE.getValue(), AccrualAccountsForSavings.FEES_RECEIVABLE.toString(), changes); - mergeSavingsToAssetAccountMappingChanges(element, SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), - savingsProductId, AccrualAccountsForSavings.PENALTIES_RECEIVABLE.getValue(), - AccrualAccountsForSavings.PENALTIES_RECEIVABLE.toString(), changes); + mergeSavingsToAssetAccountMappingChanges(element, SavingProductAccountingParams.INTEREST_RECEIVABLE.getValue(), + savingsProductId, AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue(), + AccrualAccountsForSavings.INTEREST_RECEIVABLE.toString(), changes); // income mergeSavingsToIncomeAccountMappingChanges(element, SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), diff --git a/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java b/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java index 137c42f0f63..8b18bf9a4d8 100644 --- a/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java +++ b/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.charge.service; +import java.util.Collection; import java.util.List; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; @@ -112,6 +113,8 @@ public interface ChargeReadPlatformService { */ List retrieveSavingsProductCharges(Long savingsProductId); + Collection retrieveSavingsProductAccrualCharges(Long savingsProductId); + /** Retrieve savings account charges **/ List retrieveSavingsAccountApplicableCharges(Long savingsId); diff --git a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java index 6016b5c29dd..ce7e97c1fec 100644 --- a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java @@ -312,7 +312,8 @@ public enum AccrualAccountsForSavings { ESCHEAT_LIABILITY(14), // FEES_RECEIVABLE(15), // PENALTIES_RECEIVABLE(16), // - INTEREST_PAYABLE(17); + INTEREST_PAYABLE(17), // + INTEREST_RECEIVABLE(18); private final Integer value; @@ -366,6 +367,7 @@ public enum SavingProductAccountingParams { LOSSES_WRITTEN_OFF("writeOffAccountId"), // ESCHEAT_LIABILITY("escheatLiabilityId"), // PENALTIES_RECEIVABLE("penaltiesReceivableAccountId"), // + INTEREST_RECEIVABLE("interestReceivableAccountId"), // FEES_RECEIVABLE("feesReceivableAccountId"), // INTEREST_PAYABLE("interestPayableAccountId"); @@ -404,7 +406,8 @@ public enum SavingProductAccountingDataParams { ESCHEAT_LIABILITY("escheatLiabilityAccount"), // FEES_RECEIVABLE("feeReceivableAccount"), // PENALTIES_RECEIVABLE("penaltyReceivableAccount"), // - INTEREST_PAYABLE("interestPayableAccount"); // + INTEREST_PAYABLE("interestPayableAccount"), // + INTEREST_RECEIVABLE("interestReceivableAccount"); // private final String value; diff --git a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.java b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.java new file mode 100644 index 00000000000..70c7613ae47 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.java @@ -0,0 +1,45 @@ +/** + * 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.accounting.common; + +public class AccountingValidations { + + protected AccountingValidations() {} + + public static boolean isCashBasedAccounting(final Integer accountingRuleType) { + return AccountingRuleType.CASH_BASED.getValue().equals(accountingRuleType); + } + + public static boolean isAccrualPeriodicBasedAccounting(final Integer accountingRuleType) { + return AccountingRuleType.ACCRUAL_PERIODIC.getValue().equals(accountingRuleType); + } + + public static boolean isUpfrontAccrualAccounting(final Integer accountingRuleType) { + return AccountingRuleType.ACCRUAL_UPFRONT.getValue().equals(accountingRuleType); + } + + public static boolean isAccrualBasedAccounting(final Integer accountingRuleType) { + return AccountingRuleType.ACCRUAL_PERIODIC.getValue().equals(accountingRuleType) + || AccountingRuleType.ACCRUAL_UPFRONT.getValue().equals(accountingRuleType); + } + + public static boolean isCashOrAccrualBasedAccounting(final Integer accountingRuleType) { + return isCashBasedAccounting(accountingRuleType) || isAccrualBasedAccounting(accountingRuleType); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java b/fineract-core/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java index d2fd82ce1aa..8dead5d79be 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java @@ -20,8 +20,9 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.MultiException; public interface NewCommandSourceHandler { - CommandProcessingResult processCommand(JsonCommand command); + CommandProcessingResult processCommand(JsonCommand command) throws MultiException; } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java index 75840388ddc..3049cc1239e 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java @@ -37,6 +37,7 @@ import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException; +import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.lang.NonNull; @@ -131,7 +132,7 @@ public CommandSource getInitialCommandSource(CommandWrapper wrapper, JsonCommand @Transactional public CommandProcessingResult processCommand(NewCommandSourceHandler handler, JsonCommand command, CommandSource commandSource, - AppUser user, boolean isApprovedByChecker) { + AppUser user, boolean isApprovedByChecker) throws MultiException { final CommandProcessingResult result = handler.processCommand(command); String permission = commandSource.getPermissionCode(); diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index bbae94c105b..8fa0bc73530 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -3416,6 +3416,15 @@ public CommandWrapperBuilder unblockSavingsAccount(final Long accountId) { return this; } + public CommandWrapperBuilder addAccrualsToSavingsAccount(final Long accountId) { + this.actionName = "ADD_ACCRUALS"; + this.entityName = "SAVINGSACCOUNT"; + this.savingsId = accountId; + this.entityId = null; + this.href = "/savingsaccounts/" + accountId + "?command=addAccrualTransactions"; + return this; + } + public CommandWrapperBuilder disableAdHoc(Long adHocId) { this.actionName = "DISABLE"; this.entityName = "ADHOC"; diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java index f579fa27699..3d767a7c88a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java @@ -58,7 +58,7 @@ public enum JobName { PURGE_EXTERNAL_EVENTS("Purge External Events"), // PURGE_PROCESSED_COMMANDS("Purge Processed Commands"), // ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting"), // - ; + ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"); // private final String name; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java index 0f44056280e..41f9a8bc304 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java @@ -222,7 +222,7 @@ private DepositsApiConstants() { interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName, SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), - SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), + SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(), @@ -304,14 +304,14 @@ private static Set recurringDepositProductResponseData() { * Depost Account parameters */ - private static final Set DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(localeParamName, dateFormatParamName, monthDayFormatParamName, accountNoParamName, externalIdParamName, - clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, - nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, - interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, lockinPeriodFrequencyParamName, - lockinPeriodFrequencyTypeParamName, chargesParamName, chartsParamName, depositAmountParamName, depositPeriodParamName, - depositPeriodFrequencyIdParamName, savingsAccounts, expectedFirstDepositOnDateParamName, - SavingsApiConstants.withHoldTaxParamName, maturityInstructionIdParamName, transferToSavingsIdParamName)); + private static final Set DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, + dateFormatParamName, monthDayFormatParamName, accountNoParamName, externalIdParamName, clientIdParamName, groupIdParamName, + productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, nominalAnnualInterestRateParamName, + interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, interestCalculationTypeParamName, + interestCalculationDaysInYearTypeParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, + chargesParamName, chartsParamName, depositAmountParamName, depositPeriodParamName, depositPeriodFrequencyIdParamName, + savingsAccounts, expectedFirstDepositOnDateParamName, SavingsApiConstants.withHoldTaxParamName, maturityInstructionIdParamName, + transferToSavingsIdParamName, linkedAccountParamName, transferInterestToSavingsParamName)); public static final Set FIXED_DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = fixedDepositAccountRequestData(); public static final Set FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS = fixedDepositAccountResponseData(); diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java index b0ac6cecde9..0f3547201f3 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java @@ -35,6 +35,7 @@ public class SavingsApiConstants { public static final String withdrawnByApplicantAction = ".withdrawnByApplicant"; public static final String activateAction = ".activate"; public static final String modifyApplicationAction = ".modify"; + public static final String undoActivateAction = ".undoactivate"; public static final String deleteApplicationAction = ".delete"; public static final String undoTransactionAction = ".undotransaction"; public static final String applyAnnualFeeTransactionAction = ".applyannualfee"; @@ -65,6 +66,7 @@ public class SavingsApiConstants { public static final String COMMAND_BLOCK_DEBIT = "blockDebit"; public static final String COMMAND_UNBLOCK_DEBIT = "unblockDebit"; public static final String COMMAND_UNBLOCK_CREDIT = "unblockCredit"; + public static final String COMMAND_ADD_ACCRUAL_TRANSACTION = "addAccrualTransactions"; // general public static final String localeParamName = "locale"; @@ -104,6 +106,7 @@ public class SavingsApiConstants { public static final String activeParamName = "active"; public static final String nameParamName = "name"; public static final String shortNameParamName = "shortName"; + public static final String interestReceivableAccount = "interestReceivableAccountId"; public static final String descriptionParamName = "description"; public static final String currencyCodeParamName = "currencyCode"; public static final String digitsAfterDecimalParamName = "digitsAfterDecimal"; @@ -156,6 +159,7 @@ public class SavingsApiConstants { // charges parameters public static final String chargeIdParamName = "chargeId"; public static final String chargesParamName = "charges"; + public static final String accrualChargesParamName = "accrualCharges"; public static final String savingsAccountChargeIdParamName = "savingsAccountChargeId"; public static final String chargeNameParamName = "name"; public static final String penaltyParamName = "penalty"; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java index e1394661cef..376e03cc015 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java @@ -144,6 +144,15 @@ public final class SavingsAccountData implements Serializable { private transient Long glAccountIdForSavingsControl; private transient Long glAccountIdForInterestOnSavings; + private transient Long glAccountIdForSavingsControlAcountPositiveInterestNegative; + private transient Long glAccountIdForInterestReceivablePositiveInterestNegative; + + private transient Long glAccountIdForOverdraftPorfolioNegative; + private transient Long glAccountIdForInterestReceivableNegative; + + private BigDecimal interestPosting; + private BigDecimal overdraftPosting; + public static SavingsAccountData importInstanceIndividual(Long clientId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate, BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum, EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData interestCalculationTypeEnum, @@ -297,6 +306,43 @@ public void setGlAccountIdForInterestOnSavings(final Long glAccountIdForInterest this.glAccountIdForInterestOnSavings = glAccountIdForInterestOnSavings; } + public Long getId() { + return id; + } + + public Long getGlAccountIdForSavingsControlAcountPositiveInterestNegative() { + return glAccountIdForSavingsControlAcountPositiveInterestNegative; + } + + public void setGlAccountIdForSavingsControlAcountPositiveInterestNegative( + Long glAccountIdForSavingsControlAcountPositiveInterestNegative) { + this.glAccountIdForSavingsControlAcountPositiveInterestNegative = glAccountIdForSavingsControlAcountPositiveInterestNegative; + } + + public Long getGlAccountIdForInterestReceivablePositiveInterestNegative() { + return glAccountIdForInterestReceivablePositiveInterestNegative; + } + + public void setGlAccountIdForInterestReceivablePositiveInterestNegative(Long glAccountIdForInterestReceivablePositiveInterestNegative) { + this.glAccountIdForInterestReceivablePositiveInterestNegative = glAccountIdForInterestReceivablePositiveInterestNegative; + } + + public Long getGlAccountIdForOverdraftPorfolioNegative() { + return glAccountIdForOverdraftPorfolioNegative; + } + + public void setGlAccountIdForOverdraftPorfolioNegative(Long glAccountIdForOverdraftPorfolioNegative) { + this.glAccountIdForOverdraftPorfolioNegative = glAccountIdForOverdraftPorfolioNegative; + } + + public Long getGlAccountIdForInterestReceivableNegative() { + return glAccountIdForInterestReceivableNegative; + } + + public void setGlAccountIdForInterestReceivableNegative(Long glAccountIdForInterestReceivableNegative) { + this.glAccountIdForInterestReceivableNegative = glAccountIdForInterestReceivableNegative; + } + public void setHelpers(final SavingsAccountTransactionDataSummaryWrapper savingsAccountTransactionSummaryWrapper, final SavingsHelper savingsHelper) { this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; @@ -964,4 +1010,20 @@ public void setLastSavingsAccountTransaction(SavingsAccountTransactionData lastS public boolean isIsDormancyTrackingActive() { return this.isDormancyTrackingActive; } + + public BigDecimal getInterestPosting() { + return interestPosting; + } + + public void setInterestPosting(BigDecimal interestPosting) { + this.interestPosting = interestPosting; + } + + public BigDecimal getOverdraftPosting() { + return overdraftPosting; + } + + public void setOverdraftPosting(BigDecimal overdraftPosting) { + this.overdraftPosting = overdraftPosting; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java index bf1028b5fd4..6135b803ca6 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java @@ -105,6 +105,9 @@ public final class SavingsAccountTransactionData implements Serializable { private BigDecimal overdraftAmount; private transient Long modifiedId; private transient String refNo; + private Boolean isNegativeBalance; + private Boolean flagValidationInterest; + private Boolean flagValidationOverdraft; private SavingsAccountTransactionData(final Long id, final SavingsAccountTransactionEnumData transactionType, final PaymentDetailData paymentDetailData, final Long savingsId, final String savingsAccountNo, final LocalDate transactionDate, @@ -112,7 +115,7 @@ private SavingsAccountTransactionData(final Long id, final SavingsAccountTransac final boolean reversed, final AccountTransferData transfer, final Collection paymentTypeOptions, final LocalDate submittedOnDate, final boolean interestedPostedAsOn, final String submittedByUsername, final String note, final Boolean isReversal, final Long originalTransactionId, boolean isManualTransaction, final Boolean lienTransaction, - final Long releaseTransactionId, final String reasonForBlock) { + final Long releaseTransactionId, final String reasonForBlock, final Boolean isNegativeBalance) { this.id = id; this.transactionType = transactionType; TransactionEntryType entryType = null; @@ -146,6 +149,7 @@ private SavingsAccountTransactionData(final Long id, final SavingsAccountTransac this.lienTransaction = lienTransaction; this.releaseTransactionId = releaseTransactionId; this.reasonForBlock = reasonForBlock; + this.isNegativeBalance = isNegativeBalance; } private static SavingsAccountTransactionData createData(final Long id, final SavingsAccountTransactionEnumData transactionType, @@ -156,7 +160,7 @@ private static SavingsAccountTransactionData createData(final Long id, final Sav final Boolean lienTransaction) { return new SavingsAccountTransactionData(id, transactionType, paymentDetailData, accountId, accountNo, date, currency, amount, outstandingChargeAmount, runningBalance, reversed, transfer, paymentTypeOptions, submittedOnDate, interestedPostedAsOn, - submittedByUsername, note, null, null, false, lienTransaction, null, null); + submittedByUsername, note, null, null, false, lienTransaction, null, null, false); } public static SavingsAccountTransactionData create(final Long id, final SavingsAccountTransactionEnumData transactionType, @@ -167,7 +171,8 @@ public static SavingsAccountTransactionData create(final Long id, final SavingsA final Boolean lienTransaction, final Long releaseTransactionId, final String reasonForBlock) { return new SavingsAccountTransactionData(id, transactionType, paymentDetailData, savingsId, savingsAccountNo, date, currency, amount, outstandingChargeAmount, runningBalance, reversed, transfer, null, submittedOnDate, interestedPostedAsOn, - submittedByUsername, note, isReversal, originalTransactionId, false, lienTransaction, releaseTransactionId, reasonForBlock); + submittedByUsername, note, isReversal, originalTransactionId, false, lienTransaction, releaseTransactionId, reasonForBlock, + false); } public static SavingsAccountTransactionData create(final Long id, final SavingsAccountTransactionEnumData transactionType, @@ -235,10 +240,10 @@ public static SavingsAccountTransactionData templateOnTop(final SavingsAccountTr private static SavingsAccountTransactionData createImport(final SavingsAccountTransactionEnumData transactionType, final PaymentDetailData paymentDetailData, final Long savingsAccountId, final String accountNumber, final LocalDate transactionDate, final BigDecimal transactionAmount, final boolean reversed, final LocalDate submittedOnDate, - boolean isManualTransaction, final Boolean lienTransaction) { + boolean isManualTransaction, final Boolean lienTransaction, final Boolean isNegativeBalance) { SavingsAccountTransactionData data = new SavingsAccountTransactionData(null, transactionType, paymentDetailData, savingsAccountId, accountNumber, transactionDate, null, transactionAmount, null, null, reversed, null, null, submittedOnDate, false, null, - null, null, null, isManualTransaction, lienTransaction, null, null); + null, null, null, isManualTransaction, lienTransaction, null, null, isNegativeBalance); // duplicated import fields data.savingsAccountId = savingsAccountId; data.accountNumber = accountNumber; @@ -251,14 +256,14 @@ public static SavingsAccountTransactionData copyTransaction(SavingsAccountTransa return createImport(accountTransaction.getTransactionType(), accountTransaction.getPaymentDetailData(), accountTransaction.getSavingsAccountId(), null, accountTransaction.getTransactionDate(), accountTransaction.getAmount(), accountTransaction.isReversed(), accountTransaction.getSubmittedOnDate(), accountTransaction.isManualTransaction(), - accountTransaction.getLienTransaction()); + accountTransaction.getLienTransaction(), false); } public static SavingsAccountTransactionData importInstance(BigDecimal transactionAmount, LocalDate transactionDate, Long paymentTypeId, String accountNumber, String checkNumber, String routingCode, String receiptNumber, String bankNumber, String note, Long savingsAccountId, SavingsAccountTransactionEnumData transactionType, Integer rowIndex, String locale, String dateFormat) { SavingsAccountTransactionData data = createImport(transactionType, null, savingsAccountId, accountNumber, transactionDate, - transactionAmount, false, transactionDate, false, false); + transactionAmount, false, transactionDate, false, false, false); data.rowIndex = rowIndex; data.paymentTypeId = paymentTypeId; data.checkNumber = checkNumber; @@ -272,10 +277,11 @@ public static SavingsAccountTransactionData importInstance(BigDecimal transactio } private static SavingsAccountTransactionData createImport(SavingsAccountTransactionEnumData transactionType, Long savingsAccountId, - LocalDate transactionDate, BigDecimal transactionAmount, final LocalDate submittedOnDate, boolean isManualTransaction) { + LocalDate transactionDate, BigDecimal transactionAmount, final LocalDate submittedOnDate, boolean isManualTransaction, + Boolean isNegativeBalance) { // import transaction return createImport(transactionType, null, savingsAccountId, null, transactionDate, transactionAmount, false, submittedOnDate, - isManualTransaction, false); + isManualTransaction, false, isNegativeBalance); } public static SavingsAccountTransactionData interestPosting(final SavingsAccountData savingsAccount, final LocalDate date, @@ -285,17 +291,28 @@ public static SavingsAccountTransactionData interestPosting(final SavingsAccount SavingsAccountTransactionEnumData transactionType = new SavingsAccountTransactionEnumData( savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), savingsAccountTransactionType.getValue().toString()); - return createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), submittedOnDate, isManualTransaction); + return createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), submittedOnDate, isManualTransaction, false); + } + + public static SavingsAccountTransactionData accrual(final SavingsAccountData savingsAccount, final LocalDate date, final Money amount, + final boolean isManualTransaction) { + final LocalDate submittedOnDate = DateUtils.getBusinessLocalDate(); + final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.ACCRUAL; + SavingsAccountTransactionEnumData transactionType = new SavingsAccountTransactionEnumData( + savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), + savingsAccountTransactionType.getValue().toString()); + return createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), submittedOnDate, isManualTransaction, false); } public static SavingsAccountTransactionData overdraftInterest(final SavingsAccountData savingsAccount, final LocalDate date, - final Money amount, final boolean isManualTransaction) { + final Money amount, final boolean isManualTransaction, final Boolean isNegativeBalance) { final LocalDate submittedOnDate = DateUtils.getBusinessLocalDate(); final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.OVERDRAFT_INTEREST; SavingsAccountTransactionEnumData transactionType = new SavingsAccountTransactionEnumData( savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), savingsAccountTransactionType.getValue().toString()); - return createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), submittedOnDate, isManualTransaction); + return createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), submittedOnDate, isManualTransaction, + isNegativeBalance); } public static SavingsAccountTransactionData withHoldTax(final SavingsAccountData savingsAccount, final LocalDate date, @@ -306,7 +323,7 @@ public static SavingsAccountTransactionData withHoldTax(final SavingsAccountData savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), savingsAccountTransactionType.getValue().toString()); SavingsAccountTransactionData accountTransaction = createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), - submittedOnDate, false); + submittedOnDate, false, false); accountTransaction.addTaxDetails(taxDetails); return accountTransaction; } @@ -323,6 +340,22 @@ public boolean isOverdraftInterestAndNotReversed() { return this.transactionType.isIncomeFromInterest() && isNotReversed(); } + public Boolean getFlagValidationInterest() { + return flagValidationInterest; + } + + public void setFlagValidationInterest(Boolean flagValidationInterest) { + this.flagValidationInterest = flagValidationInterest; + } + + public Boolean getFlagValidationOverdraft() { + return flagValidationOverdraft; + } + + public void setFlagValidationOverdraft(Boolean flagValidationOverdraft) { + this.flagValidationOverdraft = flagValidationOverdraft; + } + public boolean isCredit() { return transactionType.isCredit() && isNotReversed() && !isReversalTransaction(); } @@ -388,16 +421,28 @@ public EndOfDayBalance toEndOfDayBalance(final Money openingBalance) { Money endOfDayBalance = openingBalance.copy(); if (isDeposit() || isDividendPayoutAndNotReversed()) { endOfDayBalance = openingBalance.plus(getAmount()); + endOfDayBalance = Money.of(currency, this.runningBalance); } else if (isWithdrawal() || isChargeTransactionAndNotReversed()) { - - if (openingBalance.isGreaterThanZero()) { + if (isWithdrawal()) { + endOfDayBalance = Money.of(currency, this.runningBalance); + } else if (openingBalance.isGreaterThanZero()) { endOfDayBalance = openingBalance.minus(getAmount()); } else { endOfDayBalance = Money.of(currency, this.runningBalance); } } - return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays); + return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays, + currency.getDigitsAfterDecimal()); + } + + public EndOfDayBalance toEndOfDayBalanceDates(final Money openingBalance, LocalDateInterval date) { + final MonetaryCurrency currency = openingBalance.getCurrency(); + Money endOfDayBalance = Money.of(currency, this.runningBalance); + + return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, + this.balanceNumberOfDays != null ? this.balanceNumberOfDays : date.endDate().getDayOfMonth(), + currency.getDigitsAfterDecimal()); } public boolean isChargeTransactionAndNotReversed() { @@ -427,7 +472,9 @@ public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, fi if (isDeposit() || isDividendPayoutAndNotReversed()) { endOfDayBalance = endOfDayBalance.plus(getAmount()); } else if (isWithdrawal() || isChargeTransactionAndNotReversed()) { - if (endOfDayBalance.isGreaterThanZero() || isAllowOverdraft) { + if (isWithdrawal()) { + endOfDayBalance = Money.of(currency, this.runningBalance); + } else if (endOfDayBalance.isGreaterThanZero() || isAllowOverdraft) { endOfDayBalance = endOfDayBalance.minus(getAmount()); } else { endOfDayBalance = Money.of(currency, this.runningBalance); @@ -441,7 +488,8 @@ public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, fi numberOfDaysOfBalance = spanOfBalance.daysInPeriodInclusiveOfEndDate(); } - return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance); + return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance, + currency.getDigitsAfterDecimal()); } public void reverse() { diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java index bed16288e09..fc90c1f6289 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java @@ -75,6 +75,7 @@ public final class SavingsProductData implements Serializable { // charges private final Collection charges; + private final Collection accrualCharges; // template private final Collection currencyOptions; @@ -120,6 +121,7 @@ public static SavingsProductData template(final CurrencyData currency, final Enu final Map accountingMappings = null; final Collection paymentChannelToFundSourceMappings = null; final Collection charges = null; + final Collection accrualCharges = null; final Collection feeToIncomeAccountMappings = null; final Collection penaltyToIncomeAccountMappings = null; final boolean allowOverdraft = false; @@ -147,10 +149,11 @@ public static SavingsProductData template(final CurrencyData currency, final Enu penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, allowOverdraft, overdraftLimit, minRequiredBalance, enforceMinRequiredBalance, maxAllowedLienLimit, lienAllowed, minBalanceForInterestCalculation, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, taxGroup, taxGroupOptions, - isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment); + isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment, accrualCharges); } - public static SavingsProductData withCharges(final SavingsProductData product, final Collection charges) { + public static SavingsProductData withCharges(final SavingsProductData product, final Collection charges, + final Collection accrualCharges) { return new SavingsProductData(product.id, product.name, product.shortName, product.description, product.currency, product.nominalAnnualInterestRate, product.interestCompoundingPeriodType, product.interestPostingPeriodType, product.interestCalculationType, product.interestCalculationDaysInYearType, product.minRequiredOpeningBalance, @@ -165,7 +168,7 @@ public static SavingsProductData withCharges(final SavingsProductData product, f product.minBalanceForInterestCalculation, product.nominalAnnualInterestRateOverdraft, product.minOverdraftForInterestCalculation, product.withHoldTax, product.taxGroup, product.taxGroupOptions, product.isDormancyTrackingActive, product.daysToInactive, product.daysToDormancy, product.daysToEscheat, - product.accountMappingForPayment); + product.accountMappingForPayment, accrualCharges); } /** @@ -200,7 +203,8 @@ public static SavingsProductData withTemplate(final SavingsProductData existingP existingProduct.maxAllowedLienLimit, existingProduct.lienAllowed, existingProduct.minBalanceForInterestCalculation, existingProduct.nominalAnnualInterestRateOverdraft, existingProduct.minOverdraftForInterestCalculation, existingProduct.withHoldTax, existingProduct.taxGroup, taxGroupOptions, existingProduct.isDormancyTrackingActive, - existingProduct.daysToInactive, existingProduct.daysToDormancy, existingProduct.daysToEscheat, accountMappingForPayment); + existingProduct.daysToInactive, existingProduct.daysToDormancy, existingProduct.daysToEscheat, accountMappingForPayment, + existingProduct.accrualCharges); } public static SavingsProductData withAccountingDetails(final SavingsProductData existingProduct, @@ -237,7 +241,7 @@ public static SavingsProductData withAccountingDetails(final SavingsProductData existingProduct.nominalAnnualInterestRateOverdraft, existingProduct.minOverdraftForInterestCalculation, existingProduct.withHoldTax, existingProduct.taxGroup, existingProduct.taxGroupOptions, existingProduct.isDormancyTrackingActive, existingProduct.daysToInactive, existingProduct.daysToDormancy, - existingProduct.daysToEscheat, existingProduct.accountMappingForPayment); + existingProduct.daysToEscheat, existingProduct.accountMappingForPayment, existingProduct.accrualCharges); } public static SavingsProductData instance(final Long id, final String name, final String shortName, final String description, @@ -268,6 +272,7 @@ public static SavingsProductData instance(final Long id, final String name, fina final Collection chargeOptions = null; final Collection penaltyOptions = null; final Collection charges = null; + final Collection accrualCharges = null; final Collection feeToIncomeAccountMappings = null; final Collection penaltyToIncomeAccountMappings = null; final Collection taxGroupOptions = null; @@ -282,7 +287,7 @@ public static SavingsProductData instance(final Long id, final String name, fina penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, allowOverdraft, overdraftLimit, minRequiredBalance, enforceMinRequiredBalance, maxAllowedLienLimit, lienAllowed, minBalanceForInterestCalculation, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, taxGroup, taxGroupOptions, - isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment); + isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment, accrualCharges); } public static SavingsProductData lookup(final Long id, final String name) { @@ -325,6 +330,7 @@ public static SavingsProductData lookup(final Long id, final String name) { final Collection accountingRuleOptions = null; final Map> accountingMappingOptions = null; final Collection charges = null; + final Collection accrualCharges = null; final Collection chargeOptions = null; final Collection penaltyOptions = null; final Collection feeToIncomeAccountMappings = null; @@ -345,16 +351,17 @@ public static SavingsProductData lookup(final Long id, final String name) { penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, allowOverdraft, overdraftLimit, minRequiredBalance, enforceMinRequiredBalance, maxAllowedLienLimit, lienAllowed, minBalanceForInterestCalculation, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, taxGroup, taxGroupOptions, - isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment); + isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment, accrualCharges); } - public static SavingsProductData createForInterestPosting(final Long id, final EnumOptionData accountingRule) { - return new SavingsProductData(id, accountingRule); + public static SavingsProductData createForInterestPosting(final Long id, final String productName, + final EnumOptionData accountingRule) { + return new SavingsProductData(id, productName, accountingRule); } - private SavingsProductData(final Long id, final EnumOptionData accountingRule) { + private SavingsProductData(final Long id, final String productName, final EnumOptionData accountingRule) { this.id = id; - this.name = null; + this.name = productName; this.shortName = null; this.description = null; this.currency = null; @@ -386,6 +393,7 @@ private SavingsProductData(final Long id, final EnumOptionData accountingRule) { this.charges = null;// charges associated with Savings product this.chargeOptions = null;// charges available for adding to + this.accrualCharges = null; // Savings product this.penaltyOptions = null;// penalties available for adding // to Savings product @@ -433,7 +441,7 @@ private SavingsProductData(final Long id, final String name, final String shortN final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation, final boolean withHoldTax, final TaxGroupData taxGroup, final Collection taxGroupOptions, final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat, - final String accountMappingForPayment) { + final String accountMappingForPayment, final Collection accrualCharges) { this.id = id; this.name = name; this.shortName = shortName; @@ -469,6 +477,7 @@ private SavingsProductData(final Long id, final String name, final String shortN this.paymentChannelToFundSourceMappings = paymentChannelToFundSourceMappings; this.charges = charges;// charges associated with Savings product + this.accrualCharges = accrualCharges; this.chargeOptions = chargeOptions;// charges available for adding to // Savings product this.penaltyOptions = penaltyOptions;// penalties available for adding @@ -594,7 +603,7 @@ public boolean isCashBasedAccountingEnabled() { } public boolean isAccrualBasedAccountingEnabled() { - return isUpfrontAccrualAccounting() || isPeriodicAccrualAccounting(); + return isUpfrontAccrualAccounting() && isPeriodicAccrualAccounting(); } public boolean isUpfrontAccrualAccounting() { diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/AnnualCompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/AnnualCompoundingPeriod.java index 45ce4b6e0fa..5501286deb3 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/AnnualCompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/AnnualCompoundingPeriod.java @@ -47,7 +47,7 @@ public static AnnualCompoundingPeriod create(final LocalDateInterval periodInter public BigDecimal calculateInterest(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, - final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/BiAnnualCompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/BiAnnualCompoundingPeriod.java index 20ad55506f6..87044aee5ae 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/BiAnnualCompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/BiAnnualCompoundingPeriod.java @@ -47,7 +47,7 @@ public static BiAnnualCompoundingPeriod create(final LocalDateInterval periodInt public BigDecimal calculateInterest(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, - final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java index 4b918c0198a..f7eca8f4f8c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java @@ -47,9 +47,14 @@ public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency curren // total interest earned in previous periods but not yet recognised BigDecimal compoundedInterest = BigDecimal.ZERO; BigDecimal unCompoundedInterest = BigDecimal.ZERO; + LocalDate endDay = DateUtils.getBusinessLocalDate(); final CompoundInterestValues compoundInterestValues = new CompoundInterestValues(compoundedInterest, unCompoundedInterest); for (final PostingPeriod postingPeriod : allPeriods) { + if (postingPeriod.dateOfPostingTransaction().getMonth() != endDay.getMonth()) { + compoundInterestValues.setCompoundedInterest(interestEarned.getAmount()); + } + final BigDecimal interestEarnedThisPeriod = postingPeriod.calculateInterest(compoundInterestValues); final Money moneyToBePostedForPeriod = Money.of(currency, interestEarnedThisPeriod); @@ -61,8 +66,10 @@ public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency curren // calculation. if (!(postingPeriod.isInterestTransfered() || !interestTransferEnabled || (lockUntil != null && !DateUtils.isAfter(postingPeriod.dateOfPostingTransaction(), lockUntil)))) { - compoundInterestValues.setcompoundedInterest(BigDecimal.ZERO); + compoundInterestValues.setCompoundedInterest(BigDecimal.ZERO); } + endDay = postingPeriod.dateOfPostingTransaction(); + } return interestEarned; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java index 09a871e8db0..68cfc07cd77 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java @@ -38,7 +38,7 @@ public BigDecimal getuncompoundedInterest() { return this.uncompoundedInterest; } - public void setcompoundedInterest(BigDecimal interestToBeCompounded) { + public void setCompoundedInterest(BigDecimal interestToBeCompounded) { this.compoundedInterest = interestToBeCompounded; } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundingPeriod.java index 206341710e4..555a513654d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundingPeriod.java @@ -28,7 +28,7 @@ public interface CompoundingPeriod { BigDecimal calculateInterest(SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, SavingsInterestCalculationType interestCalculationType, BigDecimal interestFromPreviousPostingPeriod, BigDecimal interestRateAsFraction, long daysInYear, BigDecimal minBalanceForInterestCalculation, - BigDecimal overdraftInterestRateAsFraction, BigDecimal minOverdraftForInterestCalculation); + BigDecimal overdraftInterestRateAsFraction, BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual); LocalDateInterval getPeriodInterval(); } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/DailyCompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/DailyCompoundingPeriod.java index c46b5518a94..9f198654081 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/DailyCompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/DailyCompoundingPeriod.java @@ -77,7 +77,7 @@ private DailyCompoundingPeriod(final LocalDateInterval periodInterval, final Lis public BigDecimal calculateInterest(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestFromPreviousPostingPeriod, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, - final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; // for daily compounding - each interest calculated from previous daily diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/EndOfDayBalance.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/EndOfDayBalance.java index ce821427c42..3b435023c3c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/EndOfDayBalance.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/EndOfDayBalance.java @@ -32,17 +32,20 @@ public class EndOfDayBalance { private final Money openingBalance; private final Money endOfDayBalance; private final int numberOfDays; + private int decimals; public static EndOfDayBalance from(final LocalDate date, final Money openingBalance, final Money endOfDayBalance, - final int numberOfDays) { - return new EndOfDayBalance(date, openingBalance, endOfDayBalance, numberOfDays); + final int numberOfDays, final int decimals) { + return new EndOfDayBalance(date, openingBalance, endOfDayBalance, numberOfDays, decimals); } - public EndOfDayBalance(final LocalDate date, final Money openingBalance, final Money endOfDayBalance, final int numberOfDays) { + public EndOfDayBalance(final LocalDate date, final Money openingBalance, final Money endOfDayBalance, final int numberOfDays, + final int decimals) { this.date = date; this.openingBalance = openingBalance; this.endOfDayBalance = endOfDayBalance; this.numberOfDays = numberOfDays; + this.decimals = decimals; } public LocalDate date() { @@ -60,11 +63,15 @@ public BigDecimal cumulativeBalance(final BigDecimal interestToCompound) { MoneyHelper.getRoundingMode()); } + public void setDecimals(int decimals) { + this.decimals = decimals; + } + public BigDecimal calculateInterestOnBalance(final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { - BigDecimal interest = BigDecimal.ZERO.setScale(9, MoneyHelper.getRoundingMode()); + BigDecimal interest = BigDecimal.ZERO.setScale(this.decimals, MoneyHelper.getRoundingMode()); final BigDecimal realBalanceForInterestCalculation = this.endOfDayBalance.getAmount().add(interestToCompound); if (realBalanceForInterestCalculation.compareTo(BigDecimal.ZERO) >= 0) { if (realBalanceForInterestCalculation.compareTo(minBalanceForInterestCalculation) >= 0) { @@ -72,7 +79,7 @@ public BigDecimal calculateInterestOnBalance(final BigDecimal interestToCompound final BigDecimal dailyInterestRate = interestRateAsFraction.multiply(multiplicand, MathContext.DECIMAL64); final BigDecimal periodicInterestRate = dailyInterestRate.multiply(BigDecimal.valueOf(this.numberOfDays), MathContext.DECIMAL64); - interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64).setScale(9, + interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64).setScale(this.decimals, MoneyHelper.getRoundingMode()); } } else { @@ -81,9 +88,40 @@ public BigDecimal calculateInterestOnBalance(final BigDecimal interestToCompound final BigDecimal dailyInterestRate = overdraftInterestRateAsFraction.multiply(multiplicand, MathContext.DECIMAL64); final BigDecimal periodicInterestRate = dailyInterestRate.multiply(BigDecimal.valueOf(this.numberOfDays), MathContext.DECIMAL64); - interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64).setScale(9, + interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64).setScale(this.decimals, + MoneyHelper.getRoundingMode()); + } + } + return interest; + } + + public BigDecimal calculateInterestOnBalanceNegative(final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, + final long daysInYear, final BigDecimal minBalanceForInterestCalculation, final BigDecimal overdraftInterestRateAsFraction, + final BigDecimal minOverdraftForInterestCalculation) { + + BigDecimal interest = BigDecimal.ZERO.setScale(this.decimals, MoneyHelper.getRoundingMode()); + final BigDecimal realBalanceForInterestCalculation = this.endOfDayBalance.getAmount().add(interestToCompound); + if (realBalanceForInterestCalculation.compareTo(BigDecimal.ZERO) >= 0) { + if (realBalanceForInterestCalculation.compareTo(minBalanceForInterestCalculation) >= 0) { + final BigDecimal multiplicand = BigDecimal.ONE.divide(BigDecimal.valueOf(daysInYear), MathContext.DECIMAL64); + final BigDecimal dailyInterestRate = interestRateAsFraction.multiply(multiplicand, MathContext.DECIMAL64); + final BigDecimal periodicInterestRate = dailyInterestRate.multiply(BigDecimal.valueOf(this.numberOfDays), + MathContext.DECIMAL64); + interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64).setScale(this.decimals, MoneyHelper.getRoundingMode()); } + } else { + if (realBalanceForInterestCalculation.compareTo(minOverdraftForInterestCalculation.negate()) < 0) { + final BigDecimal balanceConvertPositive = realBalanceForInterestCalculation.abs(); + final BigDecimal porcentaje = overdraftInterestRateAsFraction.divide(BigDecimal.valueOf(100)); + final BigDecimal forDaysInYear = porcentaje.divide(BigDecimal.valueOf(daysInYear), MathContext.DECIMAL64); + final BigDecimal fordays = forDaysInYear.multiply(BigDecimal.valueOf(this.numberOfDays == 0 ? 1 : this.numberOfDays)); + + final BigDecimal calculationInteresNegative = fordays.multiply(balanceConvertPositive); + + interest = calculationInteresNegative.setScale(this.decimals, MoneyHelper.getRoundingMode()); + + } } return interest; } @@ -163,7 +201,7 @@ public EndOfDayBalance upTo(final LocalDateInterval compoundingPeriodInterval, f daysOfBalance = balancePeriodInterval.daysInPeriodInclusiveOfEndDate(); } - return new EndOfDayBalance(balanceStartDate, startingBalance, this.endOfDayBalance, daysOfBalance); + return new EndOfDayBalance(balanceStartDate, startingBalance, this.endOfDayBalance, daysOfBalance, this.decimals); } public boolean contains(final LocalDateInterval compoundingPeriodInterval) { diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/MonthlyCompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/MonthlyCompoundingPeriod.java index 7d406d026dd..6b42ebebc7e 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/MonthlyCompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/MonthlyCompoundingPeriod.java @@ -47,14 +47,15 @@ public static MonthlyCompoundingPeriod create(final LocalDateInterval periodInte public BigDecimal calculateInterest(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, - final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; switch (interestCalculationType) { case DAILY_BALANCE: interestEarned = calculateUsingDailyBalanceMethod(compoundingInterestPeriodType, interestToCompound, interestRateAsFraction, - daysInYear, minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation); + daysInYear, minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, + isAccrual); break; case AVERAGE_DAILY_BALANCE: interestEarned = calculateUsingAverageDailyBalanceMethod(interestToCompound, interestRateAsFraction, daysInYear, @@ -114,7 +115,7 @@ private BigDecimal calculateUsingAverageDailyBalanceMethod(final BigDecimal inte private BigDecimal calculateUsingDailyBalanceMethod(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, final BigDecimal overdraftInterestRateAsFraction, - final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; BigDecimal interestOnBalanceUnrounded = BigDecimal.ZERO; @@ -127,8 +128,15 @@ private BigDecimal calculateUsingDailyBalanceMethod(final SavingsCompoundingInte minOverdraftForInterestCalculation); break; case MONTHLY: - interestOnBalanceUnrounded = balance.calculateInterestOnBalance(interestToCompound, interestRateAsFraction, daysInYear, - minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation); + interestOnBalanceUnrounded = isAccrual + ? balance.calculateInterestOnBalanceNegative(interestToCompound, interestRateAsFraction, daysInYear, + minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation) + : balance.calculateInterestOnBalance(interestToCompound, interestRateAsFraction, daysInYear, + minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation); + if (isAccrual && balance.getNumberOfDays() == 0) { + interestOnBalanceUnrounded = BigDecimal.ZERO; + } + break; // case QUATERLY: // break; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java index f0bce2b1277..4e6e1a0e50d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java @@ -26,14 +26,17 @@ import java.util.Collection; import java.util.List; import java.util.TreeSet; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +@Slf4j public final class PostingPeriod { private final LocalDateInterval periodInterval; @@ -64,6 +67,26 @@ public final class PostingPeriod { private Integer financialYearBeginningMonth; + private Boolean isAccrual = false; + private Boolean isNegative = false; + private Boolean isEndTransaction = false; + + public void setAccrual(Boolean accrual) { + isAccrual = accrual; + } + + public Boolean getNegative() { + return isNegative; + } + + public void setNegative(Boolean negative) { + isNegative = negative; + } + + public void setOverdraftInterestRateAsFraction(BigDecimal overdraftInterestRateAsFraction) { + this.overdraftInterestRateAsFraction = overdraftInterestRateAsFraction; + } + public static PostingPeriod createFrom(final LocalDateInterval periodInterval, final Money periodStartingBalance, final List orderedListOfTransactions, final MonetaryCurrency currency, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, @@ -104,12 +127,14 @@ public static PostingPeriod createFrom(final LocalDateInterval periodInterval, f // period so no need to do any cropping/bounding final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalance(openingDayBalance); accountEndOfDayBalances.add(endOfDayBalance); + endOfDayBalance.setDecimals(currency.getDigitsAfterDecimal()); openingDayBalance = endOfDayBalance.closingBalance(); } else if (transaction.spansAnyPortionOf(periodInterval)) { final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalanceBoundedBy(openingDayBalance, periodInterval); accountEndOfDayBalances.add(endOfDayBalance); + endOfDayBalance.setDecimals(currency.getDigitsAfterDecimal()); closeOfDayBalance = endOfDayBalance.closingBalance(); openingDayBalance = closeOfDayBalance; @@ -139,7 +164,7 @@ public static PostingPeriod createFrom(final LocalDateInterval periodInterval, f } final EndOfDayBalance endOfDayBalance = EndOfDayBalance.from(balanceStartDate, openingDayBalance, closeOfDayBalance, - numberOfDaysOfBalance); + numberOfDaysOfBalance, currency.getDigitsAfterDecimal()); accountEndOfDayBalances.add(endOfDayBalance); @@ -153,7 +178,7 @@ public static PostingPeriod createFrom(final LocalDateInterval periodInterval, f return new PostingPeriod(periodInterval, currency, periodStartingBalance, openingDayBalance, interestCompoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYear, compoundingPeriods, interestTransfered, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, - minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); + minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth, false); } public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval, final Money periodStartingBalance, @@ -163,12 +188,13 @@ public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval final LocalDate upToInterestCalculationDate, Collection interestPostTransactions, boolean isInterestTransfer, final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final BigDecimal overdraftInterestRateAsFraction, final Money minOverdraftForInterestCalculation, boolean isUserPosting, - int financialYearBeginningMonth, final boolean isAllowOverdraft) { + int financialYearBeginningMonth, final boolean isAllowOverdraft, final boolean isEntraceNewValidation) { final List accountEndOfDayBalances = new ArrayList<>(); boolean interestTransfered = false; Money openingDayBalance = periodStartingBalance; Money closeOfDayBalance = openingDayBalance; + Boolean isEndTransaction = false; for (final SavingsAccountTransactionData transaction : orderedListOfTransactions) { @@ -177,6 +203,7 @@ public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval // period so no need to do any cropping/bounding final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalance(openingDayBalance); accountEndOfDayBalances.add(endOfDayBalance); + endOfDayBalance.setDecimals(currency.getDigitsAfterDecimal()); openingDayBalance = endOfDayBalance.closingBalance(); @@ -184,9 +211,17 @@ public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalanceBoundedBy(openingDayBalance, periodInterval, isAllowOverdraft); accountEndOfDayBalances.add(endOfDayBalance); + endOfDayBalance.setDecimals(currency.getDigitsAfterDecimal()); closeOfDayBalance = endOfDayBalance.closingBalance(); openingDayBalance = closeOfDayBalance; + } else if (!isEntraceNewValidation && !isEndTransaction && MathUtil.isLessThanZero(transaction.getRunningBalance()) + && DateUtils.isEqual(periodInterval.startDate(), transaction.getDate())) { + final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalanceDates(openingDayBalance, periodInterval); + accountEndOfDayBalances.add(endOfDayBalance); + openingDayBalance = endOfDayBalance.closingBalance(); + isEndTransaction = true; + endOfDayBalance.setDecimals(currency.getDigitsAfterDecimal()); } // this check is to make sure to add interest if withdrawal is @@ -213,7 +248,7 @@ public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval } final EndOfDayBalance endOfDayBalance = EndOfDayBalance.from(balanceStartDate, openingDayBalance, closeOfDayBalance, - numberOfDaysOfBalance); + numberOfDaysOfBalance, currency.getDigitsAfterDecimal()); accountEndOfDayBalances.add(endOfDayBalance); @@ -227,7 +262,7 @@ public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval return new PostingPeriod(periodInterval, currency, periodStartingBalance, openingDayBalance, interestCompoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYear, compoundingPeriods, interestTransfered, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, - minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); + minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth, isEndTransaction); } private PostingPeriod(final LocalDateInterval periodInterval, final MonetaryCurrency currency, final Money openingBalance, @@ -235,7 +270,8 @@ private PostingPeriod(final LocalDateInterval periodInterval, final MonetaryCurr final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestRateAsFraction, final long daysInYear, final List compoundingPeriods, boolean interestTransfered, final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final BigDecimal overdraftInterestRateAsFraction, - final Money minOverdraftForInterestCalculation, boolean isUserPosting, Integer financialYearBeginningMonth) { + final Money minOverdraftForInterestCalculation, boolean isUserPosting, Integer financialYearBeginningMonth, + Boolean isEndTransaction) { this.periodInterval = periodInterval; this.currency = currency; this.openingBalance = openingBalance; @@ -257,6 +293,7 @@ private PostingPeriod(final LocalDateInterval periodInterval, final MonetaryCurr this.minOverdraftForInterestCalculation = minOverdraftForInterestCalculation; this.isUserPosting = isUserPosting; this.financialYearBeginningMonth = financialYearBeginningMonth; + this.isEndTransaction = isEndTransaction; } public Money interest() { @@ -282,10 +319,11 @@ public BigDecimal calculateInterest(final CompoundInterestValues compoundInteres // to be applied to the balanced for interest calculation for (final CompoundingPeriod compoundingPeriod : this.compoundingPeriods) { + boolean isAccrual = this.isAccrual; final BigDecimal interestUnrounded = compoundingPeriod.calculateInterest(this.interestCompoundingType, this.interestCalculationType, compoundInterestValues.getcompoundedInterest(), this.interestRateAsFraction, this.daysInYear, this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction, - this.minOverdraftForInterestCalculation.getAmount()); + this.minOverdraftForInterestCalculation.getAmount(), isAccrual); BigDecimal unCompoundedInterest = compoundInterestValues.getuncompoundedInterest().add(interestUnrounded); compoundInterestValues.setuncompoundedInterest(unCompoundedInterest); LocalDate compoundingPeriodEndDate = compoundingPeriod.getPeriodInterval().endDate(); @@ -297,7 +335,12 @@ public BigDecimal calculateInterest(final CompoundInterestValues compoundInteres if (compoundingPeriodEndDate.equals(compoundingPeriod.getPeriodInterval().endDate())) { BigDecimal interestCompounded = compoundInterestValues.getcompoundedInterest().add(unCompoundedInterest); - compoundInterestValues.setcompoundedInterest(interestCompounded); + if (isNegative) { + compoundInterestValues.setCompoundedInterest(interestCompounded.negate()); + } else { + compoundInterestValues.setCompoundedInterest(interestCompounded); + } + compoundInterestValues.setZeroForInterestToBeUncompounded(); } interestEarned = interestEarned.add(interestUnrounded); @@ -310,7 +353,7 @@ public BigDecimal calculateInterest(final CompoundInterestValues compoundInteres } public Money getInterestEarned() { - return this.interestEarnedRounded; + return this.interestEarnedRounded != null ? this.interestEarnedRounded : Money.zero(this.currency); } private static List compoundingPeriodsInPostingPeriod(final LocalDateInterval postingPeriodInterval, @@ -545,4 +588,19 @@ public Integer getFinancialYearBeginningMonth() { return this.financialYearBeginningMonth; } + public List getCompoundingPeriods() { + return compoundingPeriods; + } + + public Money getClosingBalance() { + return closingBalance; + } + + public Money getOpeningBalance() { + return openingBalance; + } + + public Boolean getEndTransaction() { + return isEndTransaction; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/QuarterlyCompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/QuarterlyCompoundingPeriod.java index c2faa8a4711..81f0983918e 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/QuarterlyCompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/QuarterlyCompoundingPeriod.java @@ -46,7 +46,7 @@ public static QuarterlyCompoundingPeriod create(final LocalDateInterval periodIn public BigDecimal calculateInterest(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, - final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/SavingsAccountTransactionDetailsForPostingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/SavingsAccountTransactionDetailsForPostingPeriod.java index 3a3145b377f..bf3ce6f16a7 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/SavingsAccountTransactionDetailsForPostingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/SavingsAccountTransactionDetailsForPostingPeriod.java @@ -64,15 +64,17 @@ public EndOfDayBalance toEndOfDayBalance(final Money openingBalance) { if (isDeposit() || isDividendPayoutAndNotReversed()) { endOfDayBalance = openingBalance.plus(getAmount(currency)); } else if (isWithdrawal() || isChargeTransactionAndNotReversed()) { - - if (openingBalance.isGreaterThanZero() || isAllowOverdraft()) { + if (isWithdrawal()) { + endOfDayBalance = Money.of(currency, this.runningBalance); + } else if (openingBalance.isGreaterThanZero() || isAllowOverdraft()) { endOfDayBalance = openingBalance.minus(getAmount(currency)); } else { endOfDayBalance = Money.of(currency, this.runningBalance); } } - return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays); + return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays, + currency.getDigitsAfterDecimal()); } public EndOfDayBalance toEndOfDayBalance(final LocalDateInterval periodInterval, final MonetaryCurrency currency) { @@ -89,7 +91,7 @@ public EndOfDayBalance toEndOfDayBalance(final LocalDateInterval periodInterval, numberOfDays = newInterval.daysInPeriodInclusiveOfEndDate(); } - return EndOfDayBalance.from(balanceDate, openingBalance, endOfDayBalance, numberOfDays); + return EndOfDayBalance.from(balanceDate, openingBalance, endOfDayBalance, numberOfDays, currency.getDigitsAfterDecimal()); } public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, final LocalDateInterval boundedBy) { @@ -127,7 +129,8 @@ public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, fi numberOfDaysOfBalance = spanOfBalance.daysInPeriodInclusiveOfEndDate(); } - return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance); + return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance, + currency.getDigitsAfterDecimal()); } private Money getAmount(MonetaryCurrency currency) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/ChargePaymentDTO.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/ChargePaymentDTO.java index 124da74b2d2..52ed3def884 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/ChargePaymentDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/ChargePaymentDTO.java @@ -21,6 +21,7 @@ import java.math.BigDecimal; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; @RequiredArgsConstructor @Getter @@ -29,4 +30,6 @@ public class ChargePaymentDTO { private final Long chargeId; private final BigDecimal amount; private final Long loanChargeId; + @Setter + private boolean accrualRecognized; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsTransactionDTO.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsTransactionDTO.java index acbcb164c03..0a943e016e9 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsTransactionDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsTransactionDTO.java @@ -46,6 +46,7 @@ public class SavingsTransactionDTO { private final BigDecimal overdraftAmount; private final boolean isAccountTransfer; private final List taxPayments; + private final Boolean isNegativeBalance; public boolean isOverdraftTransaction() { return this.overdraftAmount != null && this.overdraftAmount.doubleValue() > 0; diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java index 8816d58f050..00ec3a78ff9 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java @@ -22,5 +22,5 @@ public interface AccountingProcessorForSavings { - void createJournalEntriesForSavings(SavingsDTO savingsDTO); + void createJournalEntriesForSavings(SavingsDTO savingsDTO, boolean isNegativeBalance); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index 300ba5e47d6..e6be3c6033a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -199,6 +199,7 @@ public SavingsDTO populateSavingsDtoFromMap(final Map accounting final boolean reversed = (Boolean) map.get("reversed"); final Long paymentTypeId = (Long) map.get("paymentTypeId"); final BigDecimal overdraftAmount = (BigDecimal) map.get("overdraftAmount"); + final Boolean isNegativeBalance = (Boolean) map.get("isNegativeBalance"); final List feePayments = new ArrayList<>(); final List penaltyPayments = new ArrayList<>(); @@ -210,8 +211,10 @@ public SavingsDTO populateSavingsDtoFromMap(final Map accounting final Long chargeId = (Long) loanChargePaid.get("chargeId"); final Long loanChargeId = (Long) loanChargePaid.get("savingsChargeId"); final boolean isPenalty = (Boolean) loanChargePaid.get("isPenalty"); + final boolean accrualRecognized = (Boolean) loanChargePaid.get("accrualRecognized"); final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount"); - final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); + ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); + chargePaymentDTO.setAccrualRecognized(accrualRecognized); if (isPenalty) { penaltyPayments.add(chargePaymentDTO); } else { @@ -238,7 +241,7 @@ public SavingsDTO populateSavingsDtoFromMap(final Map accounting } final SavingsTransactionDTO transaction = new SavingsTransactionDTO(transactionOfficeId, paymentTypeId, transactionId, transactionDate, transactionType, amount, reversed, feePayments, penaltyPayments, overdraftAmount, isAccountTransfer, - taxPayments); + taxPayments, isNegativeBalance); newSavingsTransactions.add(transaction); diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java index 4aa1b935bd8..ca54fbc17de 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java @@ -38,7 +38,7 @@ public class AccrualBasedAccountingProcessorForSavings implements AccountingProc private final AccountingProcessorHelper helper; @Override - public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { + public void createJournalEntriesForSavings(final SavingsDTO savingsDTO, boolean isNegativeBalance) { final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); final Long savingsProductId = savingsDTO.getSavingsProductId(); final Long savingsId = savingsDTO.getSavingsId(); @@ -162,9 +162,9 @@ else if (savingsTransactionDTO.getTransactionType().isInterestPosting() && savin transactionId, transactionDate, overdraftAmount, isReversal); if (isPositive) { this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), - AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); + AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); } } } @@ -182,9 +182,21 @@ else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { else if (savingsTransactionDTO.getTransactionType().isAccrual()) { // Post journal entry for Accrual Recognition if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + if (feePayments.size() > 0 || penaltyPayments.size() > 0) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.FEES_RECEIVABLE.getValue(), AccrualAccountsForSavings.INCOME_FROM_FEES.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else if (savingsTransactionDTO.getIsNegativeBalance()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue(), + AccrualAccountsForSavings.INCOME_FROM_INTEREST.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), + AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + } } } @@ -205,10 +217,15 @@ else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsT savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, penaltyPayments); if (isPositive) { + final ChargePaymentDTO chargePaymentDTO = penaltyPayments.get(0); + AccrualAccountsForSavings accountTypeToBeDebited = AccrualAccountsForSavings.SAVINGS_CONTROL; + if (chargePaymentDTO.isAccrualRecognized()) { + accountTypeToBeDebited = AccrualAccountsForSavings.FEES_RECEIVABLE; + } + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal, penaltyPayments); + accountTypeToBeDebited, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, paymentTypeId, + savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, penaltyPayments); } } else { this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, @@ -216,10 +233,15 @@ else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsT savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, feePayments); if (isPositive) { + final ChargePaymentDTO chargePaymentDTO = feePayments.get(0); + AccrualAccountsForSavings accountTypeToBeDebited = AccrualAccountsForSavings.SAVINGS_CONTROL; + if (chargePaymentDTO.isAccrualRecognized()) { + accountTypeToBeDebited = AccrualAccountsForSavings.FEES_RECEIVABLE; + } + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, - feePayments); + accountTypeToBeDebited, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, paymentTypeId, + savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, feePayments); } } } @@ -227,13 +249,23 @@ else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsT else if (savingsTransactionDTO.getTransactionType().isFeeDeduction()) { // Is the Charge a penalty? if (penaltyPayments.size() > 0) { + final ChargePaymentDTO chargePaymentDTO = penaltyPayments.get(0); + AccrualAccountsForSavings accountTypeToBeCredited = AccrualAccountsForSavings.INCOME_FROM_PENALTIES; + if (chargePaymentDTO.isAccrualRecognized()) { + accountTypeToBeCredited = AccrualAccountsForSavings.FEES_RECEIVABLE; + } this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, penaltyPayments); + AccrualAccountsForSavings.SAVINGS_CONTROL, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal, penaltyPayments); } else { + final ChargePaymentDTO chargePaymentDTO = feePayments.get(0); + AccrualAccountsForSavings accountTypeToBeCredited = AccrualAccountsForSavings.INCOME_FROM_PENALTIES; + if (chargePaymentDTO.isAccrualRecognized()) { + accountTypeToBeCredited = AccrualAccountsForSavings.FEES_RECEIVABLE; + } this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); + AccrualAccountsForSavings.SAVINGS_CONTROL, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal, feePayments); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java index 6b5f7f52e40..99e3e55ba81 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java @@ -38,7 +38,7 @@ public class CashBasedAccountingProcessorForSavings implements AccountingProcess private final AccountingProcessorHelper helper; @Override - public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { + public void createJournalEntriesForSavings(final SavingsDTO savingsDTO, boolean isNegativeBalance) { final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); final Long savingsProductId = savingsDTO.getSavingsProductId(); final Long savingsId = savingsDTO.getSavingsId(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java index 1aec8128059..4a0ead3f75e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java @@ -500,13 +500,16 @@ public void createJournalEntriesForSavings(final Map accountingB final boolean cashBasedAccountingEnabled = (Boolean) accountingBridgeData.get("cashBasedAccountingEnabled"); final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled"); + final boolean isNegativeBalance = (accountingBridgeData.get("isNegativeBalance") != null) + ? (Boolean) accountingBridgeData.get("isNegativeBalance") + : false; if (cashBasedAccountingEnabled || accrualBasedAccountingEnabled) { final SavingsDTO savingsDTO = this.helper.populateSavingsDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, accrualBasedAccountingEnabled); final AccountingProcessorForSavings accountingProcessorForSavings = this.accountingProcessorForSavingsFactory .determineProcessor(savingsDTO); - accountingProcessorForSavings.createJournalEntriesForSavings(savingsDTO); + accountingProcessorForSavings.createJournalEntriesForSavings(savingsDTO, isNegativeBalance); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java index 224742d5970..2267b86a6b0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java @@ -229,6 +229,7 @@ public void createLoanProductToGLAccountMapping(final Long loanProductId, final private void saveSavingsBaseAccountMapping(final Long savingProductId, final DepositAccountType accountType, final JsonCommand command, final JsonElement element) { // asset + this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element, SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), savingProductId, CashAccountsForSavings.SAVINGS_REFERENCE.getValue()); @@ -303,6 +304,10 @@ public void createSavingProductToGLAccountMapping(final Long savingProductId, fi case ACCRUAL_PERIODIC: saveSavingsBaseAccountMapping(savingProductId, accountType, command, element); // assets + this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element, + SavingProductAccountingParams.INTEREST_RECEIVABLE.getValue(), savingProductId, + AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue()); + this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element, SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), savingProductId, AccrualAccountsForSavings.FEES_RECEIVABLE.getValue()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferRepository.java index 7e395b20311..23256d51757 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferRepository.java @@ -39,4 +39,5 @@ public interface AccountTransferRepository @Query("select att from AccountTransferTransaction att where att.fromLoanTransaction.id IN :loanTransactions and att.reversed=false") List findByFromLoanTransactions(@Param("loanTransactions") Collection loanTransactions); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/handler/AdjustAccountTransferCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/handler/AdjustAccountTransferCommandHandler.java new file mode 100644 index 00000000000..eb6e397d91c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/handler/AdjustAccountTransferCommandHandler.java @@ -0,0 +1,43 @@ +/** + * 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.account.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "ACCOUNTTRANSFER", action = "ADJUST") +public class AdjustAccountTransferCommandHandler implements NewCommandSourceHandler { + + private final AccountTransfersWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.adjust(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformService.java index 20210dd767c..e13a0701200 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformService.java @@ -29,6 +29,8 @@ public interface AccountTransfersWritePlatformService { CommandProcessingResult create(JsonCommand command); + CommandProcessingResult adjust(JsonCommand command); + void reverseTransfersWithFromAccountType(Long accountNumber, PortfolioAccountType accountTypeId); Long transferFunds(AccountTransferDTO accountTransferDTO); 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 7eea9969e62..5e909cf08e2 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 @@ -33,7 +33,9 @@ import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Optional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.config.FineractProperties; @@ -71,6 +73,7 @@ import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.springframework.transaction.annotation.Transactional; +@Slf4j @RequiredArgsConstructor public class AccountTransfersWritePlatformServiceImpl implements AccountTransfersWritePlatformService { @@ -208,6 +211,37 @@ public CommandProcessingResult create(final JsonCommand command) { return builder.build(); } + @Transactional + @Override + public CommandProcessingResult adjust(JsonCommand command) { + + final Long accountTransferId = command.entityId(); + + Optional optAccountTransfer = this.accountTransferRepository.findById(accountTransferId); + if (!optAccountTransfer.isPresent()) { + throw new GeneralPlatformDomainRuleException("error.msg.accounttransfer.was.not.found", "Account transfer was not found"); + } + final boolean backdatedTxnsAllowedTill = this.configurationDomainService.retrievePivotDateConfig(); + + AccountTransferTransaction accountTransfer = optAccountTransfer.get(); + if (accountTransfer.getToSavingsTransaction() != null) { + log.debug("Reverse savings transfer to {} {}", accountTransfer.getToSavingsTransaction().getSavingsAccount().getAccountNumber(), + accountTransfer.getToSavingsTransaction().getId()); + savingsAccountDomainService.reverseTransfer(accountTransfer.getToSavingsTransaction(), backdatedTxnsAllowedTill); + } + if (accountTransfer.getFromSavingsTransaction() != null) { + log.debug("Reverse savings transfer from {} {}", + accountTransfer.getFromSavingsTransaction().getSavingsAccount().getAccountNumber(), + accountTransfer.getFromSavingsTransaction().getId()); + savingsAccountDomainService.reverseTransfer(accountTransfer.getFromSavingsTransaction(), backdatedTxnsAllowedTill); + } + + accountTransfer.reverse(); + this.accountTransferRepository.save(accountTransfer); + + return new CommandProcessingResultBuilder().withEntityId(accountTransferId).build(); + } + @Override @Transactional public void reverseTransfersWithFromAccountType(final Long accountNumber, final PortfolioAccountType accountTypeId) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java index 7e5f7b8748d..25a9cf58a8d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java @@ -294,6 +294,10 @@ public String savingsProductChargeSchema() { return chargeSchema() + " join m_savings_product_charge spc on spc.charge_id = c.id"; } + public String savingsProductAccrualChargeSchema() { + return chargeSchema() + " join m_savings_product_accrual_charge spc on spc.charge_id = c.id"; + } + public String shareProductChargeSchema() { return chargeSchema() + " join m_share_product_charge mspc on mspc.charge_id = c.id"; } @@ -422,6 +426,17 @@ public List retrieveSavingsProductCharges(final Long savingsProductI return this.jdbcTemplate.query(sql, rm, new Object[] { savingsProductId }); // NOSONAR } + @Override + public Collection retrieveSavingsProductAccrualCharges(final Long savingsProductId) { + final ChargeMapper rm = new ChargeMapper(); + + String sql = "select " + rm.savingsProductAccrualChargeSchema() + + " where c.is_deleted=false and c.is_active=true and spc.savings_product_id=? "; + sql += addInClauseToSQL_toLimitChargesMappedToOffice_ifOfficeSpecificProductsEnabled(); + + return this.jdbcTemplate.query(sql, rm, new Object[] { savingsProductId }); // NOSONAR + } + @Override public List retrieveShareProductCharges(final Long shareProductId) { final ChargeMapper rm = new ChargeMapper(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java index 7d0eec08f82..5ba28228f3c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java @@ -573,6 +573,9 @@ private String handleCommands(Long accountId, String externalId, String commandP } else if (is(commandParam, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT)) { final CommandWrapper commandRequest = builder.unblockSavingsAccount(accountId).build(); result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, SavingsApiConstants.COMMAND_ADD_ACCRUAL_TRANSACTION)) { + final CommandWrapper commandRequest = builder.addAccrualsToSavingsAccount(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); } if (result == null) { @@ -582,7 +585,8 @@ private String handleCommands(Long accountId, String externalId, String commandP "postInterest", "close", "assignSavingsOfficer", "unassignSavingsOfficer", SavingsApiConstants.COMMAND_BLOCK_DEBIT, SavingsApiConstants.COMMAND_UNBLOCK_DEBIT, SavingsApiConstants.COMMAND_BLOCK_CREDIT, SavingsApiConstants.COMMAND_UNBLOCK_CREDIT, - SavingsApiConstants.COMMAND_BLOCK_ACCOUNT, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT }); + SavingsApiConstants.COMMAND_BLOCK_ACCOUNT, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT, + SavingsApiConstants.COMMAND_ADD_ACCRUAL_TRANSACTION }); } return toApiJsonSerializer.serialize(result); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java index b814180e67e..b44fb8ce43a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java @@ -173,8 +173,9 @@ public String retrieveOne(@PathParam("productId") @Parameter(description = "prod SavingsProductData savingProductData = this.savingProductReadPlatformService.retrieveOne(productId); final Collection charges = this.chargeReadPlatformService.retrieveSavingsProductCharges(productId); + final Collection accrualCharges = this.chargeReadPlatformService.retrieveSavingsProductAccrualCharges(productId); - savingProductData = SavingsProductData.withCharges(savingProductData, charges); + savingProductData = SavingsProductData.withCharges(savingProductData, charges, accrualCharges); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java index 526ccb254e1..b5e74628ce2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java @@ -327,7 +327,21 @@ private GetSavingsProductsFeeToIncomeAccountMappingsIncomeAccount() {} } public GetSavingsProductsFeeToIncomeAccountMappingsCharge charge; - public GetSavingsProductsFeeToIncomeAccountMappingsIncomeAccount incomeAccount; + public GetSavingsProductsGlAccount incomeAccount; + } + + static final class GetSavingsProductsCharge { + + private GetSavingsProductsCharge() {} + + @Schema(example = "12") + public Integer id; + @Schema(example = "12.34") + public BigDecimal amount; + @Schema(example = "Annual Fee") + public String name; + @Schema(example = "false") + public Boolean active; } static final class GetSavingsProductsPenaltyToIncomeAccountMappings { 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 2855bc1e566..fde99a79011 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 @@ -149,7 +149,7 @@ public void modifyApplication(final JsonCommand command, final Map existingTransactionIds, - final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) { + final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill, + final boolean isNegativeBalance) { final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); + accountingBridgeData.put("isNegativeBalance", isNegativeBalance); this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); } @Transactional @Override public void postJournalEntries(final SavingsAccount account, final Set existingTransactionIds, - final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { + final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill, boolean isNegativeBalance) { final boolean isAccountTransfer = false; - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill, + isNegativeBalance); } @Override @@ -312,8 +317,8 @@ public SavingsAccountTransaction handleReversal(SavingsAccount account, List postingPeriods = account.calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, + postReversals); + log.debug("postInterest {}", postingPeriods.size()); + + MonetaryCurrency currency = account.getCurrency(); + Money interestPostedToDate = Money.zero(currency); + + if (backdatedTxnsAllowedTill) { + interestPostedToDate = Money.of(currency, account.getSummary().getTotalInterestPosted()); + } + + boolean recalucateDailyBalanceDetails = false; + boolean applyWithHoldTax = account.isWithHoldTaxApplicableForInterestPosting(); + final List withholdTransactions = new ArrayList<>(); + + if (backdatedTxnsAllowedTill) { + withholdTransactions.addAll(account.findWithHoldSavingsTransactionsWithPivotConfig()); + } else { + withholdTransactions.addAll(account.findWithHoldTransactions()); + } + + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + log.debug(" period: {}", interestPostingPeriod.dateOfPostingTransaction()); + + final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); + final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); + log.debug(" interestEarnedToBePostedForPeriod: {}", interestEarnedToBePostedForPeriod.toString()); + + if (!interestPostingTransactionDate.isAfter(interestPostingUpToDate)) { + interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); + + SavingsAccountTransaction postingTransaction = null; + if (backdatedTxnsAllowedTill) { + postingTransaction = account.findInterestPostingSavingsTransactionWithPivotConfig(interestPostingTransactionDate); + } else { + postingTransaction = account.findInterestPostingTransactionFor(interestPostingTransactionDate); + } + if (postingTransaction == null) { + SavingsAccountTransaction newPostingTransaction = null; + if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(currency))) { + if (interestEarnedToBePostedForPeriod.isGreaterThan(Money.zero(currency))) { + newPostingTransaction = SavingsAccountTransaction.interestPosting(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, + interestPostingPeriod.isUserPosting()); + } + } else { + newPostingTransaction = SavingsAccountTransaction.overdraftInterest(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), + interestPostingPeriod.isUserPosting()); + } + if (newPostingTransaction != null) { + if (backdatedTxnsAllowedTill) { + account.addTransactionToExisting(newPostingTransaction); + } else { + account.addTransaction(newPostingTransaction); + } + if (account.savingsProduct().isAccrualBasedAccountingEnabled()) { + if (MathUtil.isGreaterThanZero(interestEarnedToBePostedForPeriod)) { + SavingsAccountTransaction accrualTransaction = SavingsAccountTransaction.accrual(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, + interestPostingPeriod.isUserPosting(), false); + if (backdatedTxnsAllowedTill) { + account.addTransactionToExisting(accrualTransaction); + } else { + account.addTransaction(accrualTransaction); + } + } else { + log.info("Accrual for Overdraft interest"); + } + } + if (applyWithHoldTax) { + account.createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, + backdatedTxnsAllowedTill); + } + } + recalucateDailyBalanceDetails = true; + } else { + boolean correctionRequired = false; + if (postingTransaction.isInterestPostingAndNotReversed()) { + correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); + } else { + correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod.negated()); + } + log.debug(" correctionRequired {}", correctionRequired); + if (correctionRequired) { + boolean applyWithHoldTaxForOldTransaction = false; + postingTransaction.reverse(); + SavingsAccountTransaction reversal = null; + if (postReversals) { + reversal = SavingsAccountTransaction.reversal(postingTransaction); + } + final SavingsAccountTransaction withholdTransaction = account.findTransactionFor(interestPostingTransactionDate, + withholdTransactions); + if (withholdTransaction != null) { + withholdTransaction.reverse(); + applyWithHoldTaxForOldTransaction = true; + } + SavingsAccountTransaction newPostingTransaction; + if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(currency))) { + newPostingTransaction = SavingsAccountTransaction.interestPosting(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, + interestPostingPeriod.isUserPosting()); + } else { + newPostingTransaction = SavingsAccountTransaction.overdraftInterest(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), + interestPostingPeriod.isUserPosting()); + } + if (backdatedTxnsAllowedTill) { + account.addTransactionToExisting(newPostingTransaction); + if (reversal != null) { + account.addTransactionToExisting(reversal); + } + } else { + account.addTransaction(newPostingTransaction); + if (reversal != null) { + account.addTransaction(reversal); + } + } + if (account.savingsProduct().isAccrualBasedAccountingEnabled() + && MathUtil.isGreaterThanZero(interestEarnedToBePostedForPeriod)) { + log.info("TX2: {}", interestEarnedToBePostedForPeriod.getAmount()); + SavingsAccountTransaction accrualTransaction = SavingsAccountTransaction.accrual(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, + interestPostingPeriod.isUserPosting(), false); + if (backdatedTxnsAllowedTill) { + account.addTransactionToExisting(accrualTransaction); + } else { + account.addTransaction(accrualTransaction); + } + } else { + log.info("Accrual for Overdraft2 interest"); + } + if (applyWithHoldTaxForOldTransaction) { + account.createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, + backdatedTxnsAllowedTill); + } + recalucateDailyBalanceDetails = true; + } + } + } + } + + if (recalucateDailyBalanceDetails) { + // no openingBalance concept supported yet but probably will to + // allow + // for migrations. + Money openingAccountBalance = Money.zero(currency); + + if (backdatedTxnsAllowedTill) { + if (account.getSummary().getLastInterestCalculationDate() == null) { + openingAccountBalance = Money.zero(currency); + } else { + openingAccountBalance = Money.of(currency, account.getSummary().getRunningBalanceOnPivotDate()); + } + } + + // update existing transactions so derived balance fields are + // correct. + account.recalculateDailyBalances(openingAccountBalance, interestPostingUpToDate, backdatedTxnsAllowedTill, postReversals); + } + + if (!backdatedTxnsAllowedTill) { + account.getSummary().updateSummary(currency, account.savingsAccountTransactionSummaryWrapper, account.getTransactions()); + } else { + account.getSummary().updateSummaryWithPivotConfig(currency, account.savingsAccountTransactionSummaryWrapper, null, + account.savingsAccountTransactions); + } + } + + @Override + public void reverseTransfer(SavingsAccountTransaction savingsAccountTransaction, boolean backdatedTxnsAllowedTill) { + final SavingsAccount account = savingsAccountTransaction.getSavingsAccount(); + account.setHelpers(savingsAccountTransactionSummaryWrapper, savingsHelper); + + undoTransaction(account, savingsAccountTransaction); + } + + @Override + public void undoTransaction(SavingsAccount account, SavingsAccountTransaction savingsAccountTransaction) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Long savingsId = account.getId(); + final Long transactionId = savingsAccountTransaction.getId(); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(savingsAccountTransaction.getTransactionDate(), + account); + + if (!account.allowModify()) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); + } + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + + if (account.isNotActive()) { + throwValidationExceptionForActiveStatus(SavingsApiConstants.undoTransactionAction); + } + account.undoTransaction(transactionId); + + // undoing transaction is withdrawal then undo withdrawal fee transaction if any + if (savingsAccountTransaction.isWithdrawal()) { + final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId + 1, savingsId); + if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) { + account.undoTransaction(transactionId + 1); + } + } + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + boolean postReversals = false; + checkClientOrGroupActive(account); + if (savingsAccountTransaction.isPostInterestCalculationRequired() + && account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { + postInterest(account, mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions, + false); + account.activateAccountBasedOnBalance(); + savingsAccountRepository.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, false); + } + + private void throwValidationExceptionForActiveStatus(final String actionName) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + actionName); + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("account.is.not.active"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + @Override + public void checkClientOrGroupActive(final SavingsAccount account) { + final Client client = account.getClient(); + if (client != null) { + if (client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + } + final Group group = account.group(); + if (group != null) { + if (group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/AddAccrualTransactionsToSavingsAccountCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/AddAccrualTransactionsToSavingsAccountCommandHandler.java new file mode 100644 index 00000000000..25d83e436de --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/AddAccrualTransactionsToSavingsAccountCommandHandler.java @@ -0,0 +1,44 @@ +/** + * 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.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.MultiException; +import org.apache.fineract.portfolio.savings.service.SavingsAccrualWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "SAVINGSACCOUNT", action = "ADD_ACCRUALS") +@RequiredArgsConstructor +public class AddAccrualTransactionsToSavingsAccountCommandHandler implements NewCommandSourceHandler { + + private final SavingsAccrualWritePlatformService savingsAccrualWritePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) throws MultiException { + + return savingsAccrualWritePlatformService.addAccrualEntries(command.getSavingsId()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java new file mode 100644 index 00000000000..b9b61ccea86 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java @@ -0,0 +1,60 @@ +/** + * 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.jobs.addaccrualtransactionforsavings; + +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.portfolio.savings.service.SavingsAccrualWritePlatformService; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class AddAccrualTransactionForSavingsConfig { + + @Autowired + private JobRepository jobRepository; + @Autowired + private PlatformTransactionManager transactionManager; + @Autowired + private SavingsAccrualWritePlatformService savingsAccrualWritePlatformService; + + @Bean + protected Step addAccrualTransactionForSavingsStep() { + return new StepBuilder(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS.name(), jobRepository) + .tasklet(addAccrualTransactionForSavingsTasklet(), transactionManager).build(); + } + + @Bean + public Job addAccrualTransactionForSavingsJob() { + return new JobBuilder(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS.name(), jobRepository) + .start(addAccrualTransactionForSavingsStep()).incrementer(new RunIdIncrementer()).build(); + } + + @Bean + public AddAccrualTransactionForSavingsTasklet addAccrualTransactionForSavingsTasklet() { + return new AddAccrualTransactionForSavingsTasklet(savingsAccrualWritePlatformService); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java new file mode 100644 index 00000000000..5638221996b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java @@ -0,0 +1,50 @@ +/** + * 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.jobs.addaccrualtransactionforsavings; + +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.MultiException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.portfolio.savings.service.SavingsAccrualWritePlatformService; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; + +@RequiredArgsConstructor +public class AddAccrualTransactionForSavingsTasklet implements Tasklet { + + private final SavingsAccrualWritePlatformService savingsAccrualWritePlatformService; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + try { + addPeriodicAccruals(DateUtils.getBusinessLocalDate()); + } catch (MultiException e) { + throw new JobExecutionException(e); + } + return RepeatStatus.FINISHED; + } + + private void addPeriodicAccruals(final LocalDate tilldate) throws MultiException { + savingsAccrualWritePlatformService.addAccrualEntries(tilldate); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index 02bc5f34009..77f57a18441 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -234,6 +234,17 @@ public CommandProcessingResult submitRDApplication(final JsonCommand command) { account.updateAccountNo(this.accountNumberGenerator.generate(account, accountNumberFormat)); } + final Long savingsAccountId = command.longValueOfParameterNamed(DepositsApiConstants.linkedAccountParamName); + if (savingsAccountId != null) { + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, + DepositAccountType.SAVINGS_DEPOSIT); + this.depositAccountDataValidator.validatelinkedSavingsAccount(savingsAccount, account); + boolean isActive = true; + final AccountAssociations accountAssociations = AccountAssociations.associateSavingsAccount(account, savingsAccount, + AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive); + this.accountAssociationsRepository.save(accountAssociations); + } + final Long savingsId = account.getId(); final CalendarInstance calendarInstance = getCalendarInstance(command, account); this.calendarInstanceRepository.save(calendarInstance); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java index 59d7e04da25..ab73f9238de 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java @@ -21,6 +21,7 @@ import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; +import java.math.RoundingMode; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; @@ -33,6 +34,7 @@ import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.savings.DepositAccountType; @@ -60,28 +62,63 @@ public SavingsAccountData postInterest(final MathContext mc, final LocalDate int final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final SavingsAccountData savingsAccountData) { Money interestPostedToDate = Money.zero(savingsAccountData.getCurrency()); LocalDate startInterestDate = getStartInterestCalculationDate(savingsAccountData); - if (backdatedTxnsAllowedTill && savingsAccountData.getSummary().getInterestPostedTillDate() != null) { interestPostedToDate = Money.of(savingsAccountData.getCurrency(), savingsAccountData.getSummary().getTotalInterestPosted()); savingsAccountData.setStartInterestCalculationDate(savingsAccountData.getSummary().getInterestPostedTillDate()); } else { savingsAccountData.setStartInterestCalculationDate(startInterestDate); } - final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, savingsAccountData); - boolean recalucateDailyBalanceDetails = false; boolean applyWithHoldTax = isWithHoldTaxApplicableForInterestPosting(savingsAccountData); final List withholdTransactions = new ArrayList<>(); withholdTransactions.addAll(findWithHoldSavingsTransactionsWithPivotConfig(savingsAccountData)); + Boolean flagValidationInterest = false; + Boolean flagValidationOverdraft = false; for (final PostingPeriod interestPostingPeriod : postingPeriods) { final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); + if (!DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate)) { + interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); + final SavingsAccountTransactionData postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate, + savingsAccountData); + + if (postingTransaction == null) { + SavingsAccountTransactionData newPostingTransaction; + if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(savingsAccountData.getCurrency()))) { + flagValidationInterest = true; + } else { + flagValidationOverdraft = true; + } + } else { + boolean correctionRequired = false; + if (postingTransaction.isInterestPostingAndNotReversed()) { + correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); + } else { + correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod.negated()); + } + if (DateUtils.isBefore(interestPostingTransactionDate, interestPostingUpToDate)) { + correctionRequired = false; + } + if (correctionRequired) { + if (interestEarnedToBePostedForPeriod.isGreaterThanZero() || interestEarnedToBePostedForPeriod.isLessThanZero()) { + flagValidationInterest = true; + } + } + } + } + } + + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); + final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); + final Boolean isNegativeBalance = MathUtil.isLessThanZero(interestPostingPeriod.getInterestEarned()); + if (!DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate)) { interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); final SavingsAccountTransactionData postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate, @@ -92,14 +129,21 @@ public SavingsAccountData postInterest(final MathContext mc, final LocalDate int if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(savingsAccountData.getCurrency()))) { newPostingTransaction = SavingsAccountTransactionData.interestPosting(savingsAccountData, interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); + newPostingTransaction.setFlagValidationInterest(flagValidationInterest); + newPostingTransaction.setFlagValidationOverdraft(flagValidationOverdraft); } else { newPostingTransaction = SavingsAccountTransactionData.overdraftInterest(savingsAccountData, interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), - interestPostingPeriod.isUserPosting()); - } + interestPostingPeriod.isUserPosting(), isNegativeBalance); + newPostingTransaction.setFlagValidationOverdraft(flagValidationOverdraft); + newPostingTransaction.setFlagValidationInterest(flagValidationInterest); + } savingsAccountData.updateTransactions(newPostingTransaction); - + if (savingsAccountData.getSavingsProductData().isAccrualBasedAccountingEnabled()) { + savingsAccountData.updateTransactions(SavingsAccountTransactionData.accrual(savingsAccountData, + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting())); + } if (applyWithHoldTax) { createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, savingsAccountData); @@ -112,9 +156,11 @@ public SavingsAccountData postInterest(final MathContext mc, final LocalDate int } else { correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod.negated()); } + if (DateUtils.isBefore(interestPostingTransactionDate, interestPostingUpToDate)) { + correctionRequired = false; + } if (correctionRequired) { boolean applyWithHoldTaxForOldTransaction = false; - postingTransaction.reverse(); final SavingsAccountTransactionData withholdTransaction = findTransactionFor(interestPostingTransactionDate, withholdTransactions); @@ -128,14 +174,24 @@ public SavingsAccountData postInterest(final MathContext mc, final LocalDate int newPostingTransaction = SavingsAccountTransactionData.interestPosting(savingsAccountData, interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); + + newPostingTransaction.setFlagValidationOverdraft(flagValidationOverdraft); + newPostingTransaction.setFlagValidationInterest(flagValidationInterest); + } else { newPostingTransaction = SavingsAccountTransactionData.overdraftInterest(savingsAccountData, interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), - interestPostingPeriod.isUserPosting()); + interestPostingPeriod.isUserPosting(), isNegativeBalance); + newPostingTransaction.setFlagValidationOverdraft(flagValidationOverdraft); + newPostingTransaction.setFlagValidationInterest(flagValidationInterest); } - savingsAccountData.updateTransactions(newPostingTransaction); + if (savingsAccountData.getSavingsProductData().isAccrualBasedAccountingEnabled()) { + savingsAccountData.updateTransactions( + SavingsAccountTransactionData.accrual(savingsAccountData, interestPostingTransactionDate, + interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting())); + } if (applyWithHoldTaxForOldTransaction) { createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, savingsAccountData); @@ -232,7 +288,6 @@ public List calculateInterestUsing(final MathContext mc, final Lo final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( savingsAccountData.getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth, postedAsOnDates); - final List allPostingPeriods = new ArrayList<>(); Money periodStartingBalance; @@ -268,16 +323,73 @@ public List calculateInterestUsing(final MathContext mc, final Lo if (postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) { isUserPosting = true; } - final PostingPeriod postingPeriod = PostingPeriod.createFromDTO(periodInterval, periodStartingBalance, - retreiveOrderedNonInterestPostingTransactions(savingsAccountData), monetaryCurrency, compoundingPeriodType, - interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, - interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, - isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, - isUserPosting, financialYearBeginningMonth, savingsAccountData.isAllowOverdraft()); + List listOfTransactions = retreiveOrderedNonInterestPostingTransactions(savingsAccountData); + + List listOfTransactionsNegative = new ArrayList<>(); + List listOfTransactionsPositive = new ArrayList<>(); + boolean firstIsNegative = false; + boolean firstIsSet = false; + + for (SavingsAccountTransactionData lists : listOfTransactions) { + + if (MathUtil.isLessThanZero(lists.getRunningBalance()) + && periodInterval.startDate().getMonth() == lists.getDate().getMonth()) { + listOfTransactionsNegative.add(lists); + if (!firstIsSet) { + firstIsNegative = true; + firstIsSet = true; + } + } else if (periodInterval.startDate().getMonth() == lists.getDate().getMonth()) { + BigDecimal bd = new BigDecimal(String.valueOf(lists.getRunningBalance())).setScale(2, RoundingMode.DOWN); + if (!MathUtil.isZero(bd)) { + listOfTransactionsPositive.add(lists); + if (!firstIsSet) { + firstIsNegative = false; + firstIsSet = true; + } + } + } + + } + List firstList = null; + List secondList = null; - periodStartingBalance = postingPeriod.closingBalance(); + if (firstIsNegative) { + firstList = listOfTransactionsNegative; + secondList = listOfTransactionsPositive; + } else { + firstList = listOfTransactionsPositive; + secondList = listOfTransactionsNegative; + } + Boolean flagIntroduce = false; + + if (!firstList.isEmpty()) { + final PostingPeriod postingPeriod = PostingPeriod.createFromDTO(periodInterval, periodStartingBalance, firstList, + monetaryCurrency, compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), + upToInterestCalculationDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, + isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, + isUserPosting, financialYearBeginningMonth, savingsAccountData.isAllowOverdraft(), flagIntroduce); + flagIntroduce = postingPeriod.getEndTransaction(); + periodStartingBalance = postingPeriod.closingBalance(); + if (!(MathUtil.isZero(postingPeriod.getOpeningBalance().getAmount()) + && MathUtil.isZero(postingPeriod.closingBalance().getAmount()))) { + allPostingPeriods.add(postingPeriod); + } + } - allPostingPeriods.add(postingPeriod); + if (!secondList.isEmpty()) { + final PostingPeriod postingPeriod = PostingPeriod.createFromDTO(periodInterval, periodStartingBalance, secondList, + monetaryCurrency, compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), + upToInterestCalculationDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, + isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, + isUserPosting, financialYearBeginningMonth, savingsAccountData.isAllowOverdraft(), flagIntroduce); + + periodStartingBalance = postingPeriod.closingBalance(); + if (!(MathUtil.isZero(postingPeriod.getOpeningBalance().getAmount()) + && MathUtil.isZero(postingPeriod.closingBalance().getAmount()))) { + allPostingPeriods.add(postingPeriod); + } + } } this.savingsHelper.calculateInterestForAllPostingPeriods(monetaryCurrency, allPostingPeriods, @@ -428,7 +540,8 @@ protected void recalculateDailyBalances(final Money openingAccountBalance, final } if (transaction.getId() == null && overdraftAmount.isGreaterThanZero()) { transaction.updateOverdraftAmount(overdraftAmount.getAmount()); - } else if (overdraftAmount.isNotEqualTo(Money.of(savingsAccountData.getCurrency(), transaction.getOverdraftAmount()))) { + } else if (overdraftAmount.isNotEqualTo(Money.of(savingsAccountData.getCurrency(), transaction.getOverdraftAmount())) + && MathUtil.isGreaterThanZero(transaction.getRunningBalance())) { SavingsAccountTransactionData accountTransaction = SavingsAccountTransactionData.copyTransaction(transaction); if (transaction.isChargeTransaction()) { Set chargesPaidBy = transaction.getSavingsAccountChargesPaid(); @@ -444,6 +557,26 @@ protected void recalculateDailyBalances(final Money openingAccountBalance, final accountTransaction.updateRunningBalance(runningBalance); addTransactionToExisting(accountTransaction, savingsAccountData); + isTransactionsModified = true; + } else if (overdraftAmount.isNotEqualTo(Money.of(savingsAccountData.getCurrency(), transaction.getOverdraftAmount())) + && MathUtil.isLessThanZero(transaction.getRunningBalance())) { + SavingsAccountTransactionData accountTransaction = transaction; + if (transaction.isChargeTransaction()) { + Set chargesPaidBy = transaction.getSavingsAccountChargesPaid(); + final Set newChargePaidBy = new HashSet<>(); + chargesPaidBy.forEach( + x -> newChargePaidBy.add(SavingsAccountChargesPaidByData.instance(x.getChargeId(), x.getAmount()))); + transaction.getSavingsAccountChargesPaid().addAll(newChargePaidBy); + } + // if (MathUtil.isGreaterThanZero(savingsAccountData.getSummary().getAccountBalance())){ + // transaction.reverse(); + // } + if (overdraftAmount.isGreaterThanZero()) { + transaction.updateOverdraftAmount(overdraftAmount.getAmount()); + } + transaction.updateRunningBalance(runningBalance); + // addTransactionToExisting(accountTransaction, savingsAccountData); + isTransactionsModified = true; } 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 f84962affc8..59ca9dbf23f 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 @@ -31,6 +31,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.accounting.glaccount.data.GLAccountData; @@ -64,7 +66,9 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; +import org.apache.fineract.portfolio.savings.data.SavingsAccrualData; import org.apache.fineract.portfolio.savings.data.SavingsProductData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData; import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; @@ -80,12 +84,15 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +@Slf4j public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountReadPlatformService { private final PlatformSecurityContext context; private final JdbcTemplate jdbcTemplate; private final DatabaseSpecificSQLGenerator sqlGenerator; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; // mappers private final SavingsAccountTransactionTemplateMapper transactionTemplateMapper; @@ -105,7 +112,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final JdbcTemplate jdbcTemplate, final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, ColumnValidator columnValidator, - DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper) { + DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, + final NamedParameterJdbcTemplate namedParameterJdbcTemplate) { this.context = context; this.jdbcTemplate = jdbcTemplate; this.sqlGenerator = sqlGenerator; @@ -118,6 +126,7 @@ public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext conte this.paginationHelper = paginationHelper; this.savingAccountMapperForInterestPosting = new SavingAccountMapperForInterestPosting(); this.savingAccountAssembler = savingAccountAssembler; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; } @Override @@ -251,6 +260,7 @@ public List retrieveAllSavingsDataForInterestPosting(final b new Object[] { maxSavingsId, status, pageSize, yesterday }); for (SavingsAccountData savingsAccountData : savingsAccountDataList) { this.savingAccountAssembler.assembleSavings(savingsAccountData); + log.debug(" to process {} as {}", savingsAccountData.getAccountNo(), savingsAccountData.getDepositType().getValue()); } return savingsAccountDataList; } @@ -315,7 +325,7 @@ private static final class SavingAccountMapperForInterestPosting implements Resu sqlBuilder.append("where sat.is_reversed = false and sat.is_reversal = false "); sqlBuilder.append("and sat.transaction_type_enum in (1,2) "); sqlBuilder.append("and sat.savings_account_id = sa.id) as lastActiveTransactionDate, "); - sqlBuilder.append("sp.id as productId, "); + sqlBuilder.append("sp.id as productId, sp.name as productName, "); sqlBuilder.append("sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); sqlBuilder.append("sp.days_to_inactive as daysToInactive, "); sqlBuilder.append("sp.days_to_dormancy as daysToDormancy, "); @@ -334,7 +344,10 @@ private static final class SavingAccountMapperForInterestPosting implements Resu sqlBuilder.append( "msac.id as chargeId, msac.amount as chargeAmount, msac.charge_time_enum as chargeTimeType, msac.is_penalty as isPenaltyCharge, "); sqlBuilder.append("txd.id as taxDetailsId, txd.amount as taxAmount, "); - sqlBuilder.append("apm.gl_account_id as glAccountIdForInterestOnSavings, apm1.gl_account_id as glAccountIdForSavingsControl, "); + sqlBuilder.append( + "apm2.gl_account_id as glAccountIdForInterestReceivableNegative, apm1.gl_account_id as glAccountIdForInterestOnSavings, apm.gl_account_id as glAccountIdForSavingsControl, apm3.gl_account_id as glAccountIdForOverdraftPorfolioNegative, "); + sqlBuilder.append( + "apm4.gl_account_id as glAccountIdForSavingsControlAcountPositiveInterestNegative, apm5.gl_account_id as glAccountIdForInterestReceivablePositiveInterestNegative, "); sqlBuilder.append( "mtc.id as taxComponentId, mtc.debit_account_id as debitAccountId, mtc.credit_account_id as creditAccountId, mtc.percentage as taxPercentage "); sqlBuilder.append("from m_savings_account sa "); @@ -351,9 +364,13 @@ private static final class SavingAccountMapperForInterestPosting implements Resu sqlBuilder.append("left join m_savings_account_transaction_tax_details txd on txd.savings_transaction_id = tr.id "); sqlBuilder.append("left join m_tax_component mtc on mtc.id = txd.tax_component_id "); sqlBuilder.append( - "left join acc_product_mapping apm on apm.product_type = 2 and apm.product_id = sp.id and apm.financial_account_type=3 "); + "left join acc_product_mapping apm on apm.product_type = 2 and apm.product_id = sp.id and apm.financial_account_type=2 "); sqlBuilder.append( - "left join acc_product_mapping apm1 on apm1.product_type = 2 and apm1.product_id = sp.id and apm1.financial_account_type=2 "); + "left join acc_product_mapping apm1 on apm1.product_type = 2 and apm1.product_id = sp.id and apm1.financial_account_type=17 "); + sqlBuilder.append("left join acc_product_mapping apm2 on apm2.product_id = sp.id and apm2.financial_account_type=18 "); + sqlBuilder.append("left join acc_product_mapping apm3 on apm3.product_id = sp.id and apm3.financial_account_type = 11 "); + sqlBuilder.append("left join acc_product_mapping apm4 on apm4.product_id = sp.id and apm4.financial_account_type = 2 "); + sqlBuilder.append("left join acc_product_mapping apm5 on apm5.product_id = sp.id and apm5.financial_account_type = 18 "); this.schemaSql = sqlBuilder.toString(); } @@ -407,12 +424,21 @@ public List extractData(final ResultSet rs) throws SQLExcept final Long glAccountIdForInterestOnSavings = rs.getLong("glAccountIdForInterestOnSavings"); final Long glAccountIdForSavingsControl = rs.getLong("glAccountIdForSavingsControl"); + final Long glAccountIdForOverdraftPorfolioNegative = rs.getLong("glAccountIdForOverdraftPorfolioNegative"); + final Long glAccountIdForInterestReceivableNegative = rs.getLong("glAccountIdForInterestReceivableNegative"); + + final Long glAccountIdForSavingsControlAcountPositiveInterestNegative = rs + .getLong("glAccountIdForSavingsControlAcountPositiveInterestNegative"); + final Long glAccountIdForInterestReceivablePositiveInterestNegative = rs + .getLong("glAccountIdForInterestReceivablePositiveInterestNegative"); + final Long productId = rs.getLong("productId"); + final String productName = rs.getString("productName"); final Integer accountType = rs.getInt("accountingType"); final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountType); final EnumOptionData enumOptionDataForAccounting = new EnumOptionData(accountType.longValue(), accountingRuleType.getCode(), accountingRuleType.getValue().toString()); - final SavingsProductData savingsProductData = SavingsProductData.createForInterestPosting(productId, + final SavingsProductData savingsProductData = SavingsProductData.createForInterestPosting(productId, productName, enumOptionDataForAccounting); final Integer statusEnum = JdbcSupport.getInteger(rs, "statusEnum"); @@ -563,6 +589,15 @@ public List extractData(final ResultSet rs) throws SQLExcept savingsAccountData.setClientData(clientData); savingsAccountData.setGroupGeneralData(groupGeneralData); savingsAccountData.setSavingsProduct(savingsProductData); + + savingsAccountData.setGlAccountIdForInterestReceivableNegative(glAccountIdForInterestReceivableNegative); + savingsAccountData.setGlAccountIdForOverdraftPorfolioNegative(glAccountIdForOverdraftPorfolioNegative); + + savingsAccountData.setGlAccountIdForSavingsControlAcountPositiveInterestNegative( + glAccountIdForSavingsControlAcountPositiveInterestNegative); + savingsAccountData.setGlAccountIdForInterestReceivablePositiveInterestNegative( + glAccountIdForInterestReceivablePositiveInterestNegative); + savingsAccountData.setGlAccountIdForInterestOnSavings(glAccountIdForInterestOnSavings); savingsAccountData.setGlAccountIdForSavingsControl(glAccountIdForSavingsControl); } @@ -1386,4 +1421,107 @@ public List getAccountsIdsByStatusPaged(Integer status, int pageSize, Long public Long retrieveAccountIdByExternalId(final ExternalId externalId) { return savingsAccountRepositoryWrapper.findIdByExternalId(externalId); } + + @Override + public Collection retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings) { + final SavingAccrualMapper mapper = new SavingAccrualMapper(); + final StringBuilder sqlBuilder = new StringBuilder(400); + Map paramMap = new HashMap<>(3); + sqlBuilder.append(" select " + mapper.schema() + " where "); + + sqlBuilder.append(" savings.status_enum = :active "); + sqlBuilder.append(" and (savings.nominal_annual_interest_rate is not null and savings.nominal_annual_interest_rate > 0) "); + sqlBuilder.append(" and msp.accounting_type = :type "); + sqlBuilder.append(" and (savings.closedon_date <= :tillDate or savings.closedon_date is null) "); + sqlBuilder.append(" and (savings.accrued_till_date <= :tillDate or savings.accrued_till_date is null) "); + if (savings != null) { + sqlBuilder.append(" and savings.id = " + savings.getId()); + } + sqlBuilder.append(" order by savings.id "); + paramMap.put("active", SavingsAccountStatusType.ACTIVE.getValue()); + paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue()); + paramMap.put("tillDate", tillDate); + try { + return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper); + } catch (EmptyResultDataAccessException e) { + return new ArrayList<>(); + } + } + + private static final class SavingAccrualMapper implements RowMapper { + + private final String schemaSql; + + SavingAccrualMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append( + " savings.id as savingsId, savings.status_enum as status, (CASE WHEN savings.client_id is null THEN mg.office_id ELSE mc.office_id END) as officeId, "); + sqlBuilder.append( + " savings.accrued_till_date as accruedTill, savings.product_id as productId, savings.deposit_type_enum as depositType, "); + sqlBuilder.append(" savings.account_no as accountNo, savings.nominal_annual_interest_rate as nominalAnnualIterestRate, "); + sqlBuilder.append(" savings.interest_compounding_period_enum as interestCompoundingPeriodType, "); + sqlBuilder.append(" savings.interest_posting_period_enum as interestPostingPeriodType, "); + sqlBuilder.append(" savings.interest_calculation_type_enum as interestCalculationType, "); + sqlBuilder.append(" savings.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); + sqlBuilder.append(" savings.min_balance_for_interest_calculation as minBalanceForInterestCalculation, "); + sqlBuilder.append(" savings.interest_posted_till_date as postedTill, tg.id as taxGroupId, "); + sqlBuilder.append( + " savings.currency_code as currencyCode, savings.currency_digits as currencyDigits, savings.currency_multiplesof as inMultiplesOf, "); + sqlBuilder.append( + " curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode "); + sqlBuilder.append(" from m_savings_account savings "); + sqlBuilder.append(" left join m_savings_product msp on msp.id = savings.product_id "); + sqlBuilder.append(" left join m_client mc on mc.id = savings.client_id "); + sqlBuilder.append(" left join m_group mg on mg.id = savings.group_id "); + sqlBuilder.append(" left join m_currency curr on curr.code = savings.currency_code "); + sqlBuilder.append(" left join m_tax_group tg on tg.id = savings.tax_group_id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccrualData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long savingsId = rs.getLong("savingsId"); + final String accountNo = rs.getString("accountNo"); + final Long productId = rs.getLong("productId"); + final Long officeId = rs.getLong("officeId"); + final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill"); + final LocalDate postedTill = JdbcSupport.getLocalDate(rs, "postedTill"); + final Integer depositTypeId = rs.getInt("depositType"); + final EnumOptionData depositType = SavingsEnumerations.depositType(depositTypeId); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, + currencyNameCode); + + final BigDecimal nominalAnnualIterestRate = rs.getBigDecimal("nominalAnnualIterestRate"); + + final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( + SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestCompoundingPeriodType"))); + + final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( + SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestPostingPeriodType"))); + + final EnumOptionData interestCalculationType = SavingsEnumerations + .interestCalculationType(SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationType"))); + + final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations.interestCalculationDaysInYearType( + SavingsInterestCalculationDaysInYearType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationDaysInYearType"))); + + return new SavingsAccrualData(savingsId, accountNo, depositType, null, productId, officeId, accruedTill, postedTill, currency, + nominalAnnualIterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, + interestCalculationDaysInYearType, BigDecimal.ZERO); + } + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java new file mode 100644 index 00000000000..8a51d0172b4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.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.service; + +import java.time.LocalDate; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.MultiException; + +public interface SavingsAccrualWritePlatformService { + + void addAccrualEntries(LocalDate tillDate) throws MultiException; + + CommandProcessingResult addAccrualEntries(Long savingsAccountId) throws MultiException; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java new file mode 100644 index 00000000000..01190357592 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java @@ -0,0 +1,252 @@ +/** + * 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.service; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; +import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType; +import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; +import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; +import org.apache.fineract.portfolio.savings.data.SavingsAccrualData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; +import org.apache.fineract.portfolio.savings.domain.SavingsHelper; +import org.apache.fineract.portfolio.savings.domain.interest.CompoundInterestValues; +import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; +import org.apache.fineract.portfolio.savings.domain.interest.SavingsAccountTransactionDetailsForPostingPeriod; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SavingsAccrualWritePlatformServiceImpl implements SavingsAccrualWritePlatformService { + + private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; + private final SavingsAccountAssembler savingsAccountAssembler; + private final SavingsAccountRepositoryWrapper savingsAccountRepository; + private final SavingsHelper savingsHelper; + private final ConfigurationDomainService configurationDomainService; + private final SavingsAccountDomainService savingsAccountDomainService; + + @Transactional + @Override + public void addAccrualEntries(LocalDate tillDate) throws JobExecutionException { + final Collection savingsAccrualData = savingsAccountReadPlatformService.retrievePeriodicAccrualData(tillDate, + null); + final Integer financialYearBeginningMonth = configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final MathContext mc = MoneyHelper.getMathContext(); + + List errors = new ArrayList<>(); + for (SavingsAccrualData savingsAccrual : savingsAccrualData) { + try { + SavingsAccount savingsAccount = savingsAccountAssembler.assembleFrom(savingsAccrual.getId(), false); + LocalDate fromDate = savingsAccrual.getAccruedTill(); + if (fromDate == null) { + fromDate = savingsAccount.getActivationDate(); + } + log.debug("Processing savings account {} from date {} till date {}", savingsAccrual.getAccountNo(), fromDate, tillDate); + addAccrualTransactions(savingsAccount, fromDate, tillDate, financialYearBeginningMonth, + isSavingsInterestPostingAtCurrentPeriodEnd, mc); + } catch (Exception e) { + log.error("Failed to add accrual transaction for savings {} : {}", savingsAccrual.getAccountNo(), e.getMessage()); + errors.add(e.getCause()); + } + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } + } + + @Transactional + @Override + public CommandProcessingResult addAccrualEntries(Long savingsAccountId) throws JobExecutionException { + SavingsAccount savingsAccount = savingsAccountAssembler.assembleFrom(savingsAccountId, false); + final LocalDate tillDate = DateUtils.getBusinessLocalDate(); + final Collection savingsAccrualData = savingsAccountReadPlatformService.retrievePeriodicAccrualData(tillDate, + savingsAccount); + final Integer financialYearBeginningMonth = configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final MathContext mc = MoneyHelper.getMathContext(); + + List errors = new ArrayList<>(); + for (SavingsAccrualData savingsAccrual : savingsAccrualData) { + try { + LocalDate fromDate = savingsAccrual.getAccruedTill(); + if (fromDate == null) { + fromDate = savingsAccount.getActivationDate(); + } + log.debug("Processing savings account {} from date {} till date {}", savingsAccrual.getAccountNo(), fromDate, tillDate); + addAccrualTransactions(savingsAccount, fromDate, tillDate, financialYearBeginningMonth, + isSavingsInterestPostingAtCurrentPeriodEnd, mc); + } catch (Exception e) { + log.error("Failed to add accrual transaction for savings {} : {}", savingsAccrual.getAccountNo(), e.getMessage()); + errors.add(e.getCause()); + } + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } + + return CommandProcessingResult.empty(); + } + + private void addAccrualTransactions(SavingsAccount savingsAccount, final LocalDate fromDate, final LocalDate tillDate, + final Integer financialYearBeginningMonth, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final MathContext mc) { + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + Boolean isNegativeBalance = false; + existingTransactionIds.addAll(savingsAccount.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(savingsAccount.findExistingReversedTransactionIds()); + + List postedAsOnTransactionDates = savingsAccount.getManualPostingDates(); + final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType + .fromInt(savingsAccount.getInterestCalculationType()); + + final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType + .fromInt(savingsAccount.getInterestPostingPeriodType()); + + final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType + .fromInt(savingsAccount.getInterestCalculationDaysInYearType()); + + final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(fromDate, tillDate, + postingPeriodType, financialYearBeginningMonth, postedAsOnTransactionDates); + + final List allPostingPeriods = new ArrayList<>(); + final MonetaryCurrency currency = savingsAccount.getCurrency(); + Money periodStartingBalance = Money.zero(currency); + + final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType + .fromInt(savingsAccount.getInterestCalculationType()); + final BigDecimal interestRateAsFraction = savingsAccount.getEffectiveInterestRateAsFraction(mc, tillDate); + final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(savingsAccount.getId()); + boolean isInterestTransfer = false; + final Money minBalanceForInterestCalculation = Money.of(currency, savingsAccount.getMinBalanceForInterestCalculation()); + List savingsAccountTransactionDetailsForPostingPeriodList = savingsAccount + .toSavingsAccountTransactionDetailsForPostingPeriodList(); + for (final LocalDateInterval periodInterval : postingPeriodIntervals) { + if (DateUtils.isDateInTheFuture(periodInterval.endDate())) { + continue; + } + final boolean isUserPosting = (postedAsOnTransactionDates.contains(periodInterval.endDate())); + + final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, + savingsAccountTransactionDetailsForPostingPeriodList, currency, compoundingPeriodType, interestCalculationType, + interestRateAsFraction, daysInYearType.getValue(), tillDate, interestPostTransactions, isInterestTransfer, + minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting, + financialYearBeginningMonth); + + postingPeriod.setOverdraftInterestRateAsFraction(savingsAccount.getNominalAnnualInterestRateOverdraft()); + + periodStartingBalance = postingPeriod.closingBalance(); + + allPostingPeriods.add(postingPeriod); + } + BigDecimal compoundedInterest = BigDecimal.ZERO; + BigDecimal unCompoundedInterest = BigDecimal.ZERO; + final CompoundInterestValues compoundInterestValues = new CompoundInterestValues(compoundedInterest, unCompoundedInterest); + + final List accrualTransactionDates = savingsAccount.retreiveOrderedAccrualTransactions().stream() + .map(transaction -> transaction.getTransactionDate()).toList(); + + final List accrualTransactionDatesReverse = savingsAccount.retreiveOrderedAccrualTransactions().stream() + .filter(transaction -> transaction.isReversed()).map(transaction -> transaction.getTransactionDate()).toList(); + + LocalDate accruedTillDate = fromDate; + for (PostingPeriod period : allPostingPeriods) { + LocalDate valueDate = period.getPeriodInterval().endDate(); + List foundDate = accrualTransactionDates.stream().filter(date -> date.equals(valueDate)).toList(); + List foundDateReverse = accrualTransactionDatesReverse.stream().filter(date -> date.equals(valueDate)).toList(); + if (MathUtil.isGreaterThanZero(period.closingBalance())) { + isNegativeBalance = false; + period.setAccrual(true); + period.setNegative(MathUtil.isLessThanZero(savingsAccount.getSummary().getAccountBalance())); + period.calculateInterest(compoundInterestValues); + log.debug(" period {} {} : {}", period.getPeriodInterval().startDate(), period.getPeriodInterval().endDate(), + period.getInterestEarned()); + if (!accrualTransactionDates.contains(period.getPeriodInterval().endDate())) { + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, + savingsAccount.office(), period.getPeriodInterval().endDate(), period.getInterestEarned(), false, false); + savingsAccountTransaction.setRunningBalance(period.getClosingBalance()); + savingsAccount.addTransaction(savingsAccountTransaction); + } else if (accrualTransactionDatesReverse.contains(period.getPeriodInterval().endDate())) { + if (foundDate.size() == foundDateReverse.size()) { + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, + savingsAccount.office(), period.getPeriodInterval().endDate(), period.getInterestEarned(), false, false); + savingsAccountTransaction.setRunningBalance(period.getClosingBalance()); + savingsAccount.addTransaction(savingsAccountTransaction); + } + } + } else { + + isNegativeBalance = true; + period.setAccrual(true); + period.setNegative(MathUtil.isLessThanZero(savingsAccount.getSummary().getAccountBalance())); + period.calculateInterest(compoundInterestValues); + log.debug(" period {} {} : {}", period.getPeriodInterval().startDate(), period.getPeriodInterval().endDate(), + period.getInterestEarned()); + if (!accrualTransactionDates.contains(period.getPeriodInterval().endDate()) + && !MathUtil.isZero(period.getInterestEarned().getAmount())) { + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, + savingsAccount.office(), period.getPeriodInterval().endDate(), period.getInterestEarned(), false, true); + savingsAccountTransaction.setRunningBalance(period.getClosingBalance()); + savingsAccount.addTransaction(savingsAccountTransaction); + } else if (accrualTransactionDatesReverse.contains(period.getPeriodInterval().endDate())) { + if (foundDate.size() == foundDateReverse.size()) { + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, + savingsAccount.office(), period.getPeriodInterval().endDate(), period.getInterestEarned(), false, true); + savingsAccountTransaction.setRunningBalance(period.getClosingBalance()); + savingsAccount.addTransaction(savingsAccountTransaction); + } + } + + } + } + + savingsAccount.setAccruedTillDate(accruedTillDate); + savingsAccountRepository.saveAndFlush(savingsAccount); + + savingsAccountDomainService.postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false, + isNegativeBalance); + } + +} 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 c7fb7d249e6..d45f3c4e6b9 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 @@ -678,7 +678,7 @@ public CommandProcessingResult createActiveApplication(final SavingsAccountDataD generateAccountNumber(account); // post journal entries for activation charges - this.savingsAccountDomainService.postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + this.savingsAccountDomainService.postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, false); return new CommandProcessingResultBuilder() // .withSavingsId(account.getId()) // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java index 07ab31de7b3..ac6492562cf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java @@ -20,6 +20,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_PRODUCT_RESOURCE_NAME; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountingRuleParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accrualChargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName; @@ -140,7 +141,7 @@ public CommandProcessingResult update(final Long productId, final JsonCommand co final Map changes = product.update(command); - if (changes.containsKey(chargesParamName)) { + if (changes.containsKey(chargesParamName) || changes.containsKey(accrualChargesParamName)) { final Set savingsProductCharges = this.savingsProductAssembler.assembleListOfSavingsProductCharges(command, product.currency().getCode()); final boolean updated = product.update(savingsProductCharges); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index e1bc759a7de..0c6b89cc8ec 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -147,6 +147,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @Configuration public class SavingsConfiguration { @@ -338,9 +339,10 @@ public SavingsAccountInterestPostingService savingsAccountInterestPostingService @ConditionalOnMissingBean(SavingsAccountReadPlatformService.class) public SavingsAccountReadPlatformService savingsAccountReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator, - SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, ColumnValidator columnValidator) { + SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, ColumnValidator columnValidator, + NamedParameterJdbcTemplate namedParameterJdbcTemplate) { return new SavingsAccountReadPlatformServiceImpl(context, jdbcTemplate, savingAccountAssembler, paginationHelper, columnValidator, - sqlGenerator, savingsAccountRepositoryWrapper); + sqlGenerator, savingsAccountRepositoryWrapper, namedParameterJdbcTemplate); } @Bean diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java index 4f45f832667..f6425fe0f25 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java @@ -54,6 +54,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder; import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException; +import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.useradministration.domain.AppUser; @@ -153,7 +154,7 @@ public void teardown() { } @Test - public void testExecuteCommandSuccessAfter2Fails() { + public void testExecuteCommandSuccessAfter2Fails() throws MultiException { CommandWrapper commandWrapper = getCommandWrapper(); long commandId = 1L; @@ -198,7 +199,7 @@ public void testExecuteCommandSuccessAfter2Fails() { * stays in the same status therefor it should fail after reaching max retry count. */ @Test - public void executeCommandShouldFailAfterRetriesWithIdempotentCommandProcessUnderProcessingException() { + public void executeCommandShouldFailAfterRetriesWithIdempotentCommandProcessUnderProcessingException() throws MultiException { CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class); when(commandWrapper.isDatatableResource()).thenReturn(false); when(commandWrapper.isNoteResource()).thenReturn(false); @@ -254,7 +255,7 @@ public void executeCommandShouldFailAfterRetriesWithIdempotentCommandProcessUnde * processable. */ @Test - public void executeCommandShouldPassAfter1retryFailsByIdempotentCommandProcessUnderProcessingException() { + public void executeCommandShouldPassAfter1retryFailsByIdempotentCommandProcessUnderProcessingException() throws MultiException { CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class); when(commandWrapper.isDatatableResource()).thenReturn(false); when(commandWrapper.isNoteResource()).thenReturn(false); @@ -312,7 +313,7 @@ public void executeCommandShouldPassAfter1retryFailsByIdempotentCommandProcessUn * fail, status should be still the same. On 3rd try it should result no error. */ @Test - public void executeCommandShouldPassAfter2RetriesOnRetryExceptionAndWithStuckStatus() { + public void executeCommandShouldPassAfter2RetriesOnRetryExceptionAndWithStuckStatus() throws MultiException { CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class); when(commandWrapper.isDatatableResource()).thenReturn(false); when(commandWrapper.isNoteResource()).thenReturn(false); @@ -375,7 +376,7 @@ public void executeCommandShouldPassAfter2RetriesOnRetryExceptionAndWithStuckSta } @Test - public void testExecuteCommandSuccess() { + public void testExecuteCommandSuccess() throws MultiException { CommandWrapper commandWrapper = getCommandWrapper(); long commandId = 1L; @@ -415,7 +416,7 @@ public void testExecuteCommandSuccess() { } @Test - public void testExecuteCommandFails() { + public void testExecuteCommandFails() throws MultiException { CommandWrapper commandWrapper = getCommandWrapper(); JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); Long commandId = jsonCommand.commandId(); @@ -482,7 +483,7 @@ public void publishHookEventHandlesInvalidJson() { private static final class RetryException extends RuntimeException {} @Test - public void testExecuteCommandWithRetry() { + public void testExecuteCommandWithRetry() throws MultiException { CommandWrapper commandWrapper = getCommandWrapper(); when(commandWrapper.isInterestPauseResource()).thenReturn(false); @@ -540,7 +541,7 @@ public void testExecuteCommandWithRetry() { } @Test - public void testExecuteCommandWithMaxRetryFailure() { + public void testExecuteCommandWithMaxRetryFailure() throws MultiException { CommandWrapper commandWrapper = getCommandWrapper(); when(commandWrapper.isInterestPauseResource()).thenReturn(false); diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java new file mode 100644 index 00000000000..9da50c0b5c1 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java @@ -0,0 +1,52 @@ +/** + * 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.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.tax.data.TaxGroupData; + +@Data +@RequiredArgsConstructor +public class SavingsAccrualData { + + private final Long id; + private final String accountNo; + private final EnumOptionData depositType; + private final SavingsAccountStatusEnumData status; + private final Long savingsProductId; + private final Long officeId; + private final LocalDate accruedTill; + private final LocalDate postedTill; + private final CurrencyData currencyData; + private final BigDecimal nominalAnnualInterestRate; + private final EnumOptionData interestCompoundingPeriodType; + private final EnumOptionData interestPostingPeriodType; + private final EnumOptionData interestCalculationType; + private final EnumOptionData interestCalculationDaysInYearType; + + private final BigDecimal accruedInterestIncome; + private LocalDate interestCalculatedFrom; + private TaxGroupData taxGroup; + +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java index 87685317d5e..a8b7cdf1bd2 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java @@ -33,6 +33,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationTypeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCompoundingPeriodTypeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestPostingPeriodTypeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestReceivableAccount; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.isDormancyTrackingActiveParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyParamName; @@ -89,7 +90,7 @@ public class SavingsProductDataValidator { private final SavingsProductAccountingDataValidator savingsProductAccountingDataValidator; private static final Set SAVINGS_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList( SavingsApiConstants.localeParamName, SavingsApiConstants.monthDayFormatParamName, nameParamName, shortNameParamName, - descriptionParamName, currencyCodeParamName, digitsAfterDecimalParamName, inMultiplesOfParamName, + interestReceivableAccount, descriptionParamName, currencyCodeParamName, digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, SavingsApiConstants.withdrawalFeeAmountParamName, @@ -98,8 +99,9 @@ public class SavingsProductDataValidator { SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), - SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), - SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(), + SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), + SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), + SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(), SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), @@ -109,7 +111,8 @@ public class SavingsProductDataValidator { nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, SavingsApiConstants.minRequiredBalanceParamName, SavingsApiConstants.enforceMinRequiredBalanceParamName, SavingsApiConstants.maxAllowedLienLimitParamName, SavingsApiConstants.lienAllowedParamName, - minBalanceForInterestCalculationParamName, withHoldTaxParamName, taxGroupIdParamName)); + minBalanceForInterestCalculationParamName, withHoldTaxParamName, taxGroupIdParamName, + SavingsApiConstants.accrualChargesParamName)); public void validateForCreate(final String json) { diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountInterestRateChart.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountInterestRateChart.java index 4e2e1389f02..1cb23e7c27b 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountInterestRateChart.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountInterestRateChart.java @@ -156,4 +156,12 @@ public BigDecimal getApplicableInterestRate(final BigDecimal depositAmount, fina public boolean isPrimaryGroupingByAmount() { return this.chartFields.isPrimaryGroupingByAmount(); } + + public SavingsAccount getAccount() { + return account; + } + + public void setAccount(SavingsAccount account) { + this.account = account; + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositTermDetail.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositTermDetail.java index 360e64ada17..51e498bf3eb 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositTermDetail.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositTermDetail.java @@ -166,6 +166,10 @@ public Integer inMultiplesOfDepositTermType() { return this.inMultiplesOfDepositTermType; } + public Integer getDepositPeriodInDays(final Integer depositPeriod, final SavingsPeriodFrequencyType depositPeriodFrequencyType) { + return this.convertToSafeDays(depositPeriod, depositPeriodFrequencyType); + } + public boolean isDepositBetweenMinAndMax(LocalDate depositStartDate, LocalDate depositEndDate) { return isEqualOrGreaterThanMin(depositStartDate, depositEndDate) && isEqualOrLessThanMax(depositStartDate, depositEndDate); } @@ -263,4 +267,5 @@ public DepositTermDetail copy() { return DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType); } + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java index 216770856c3..07d956a3c6c 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java @@ -113,7 +113,7 @@ protected FixedDepositProduct(final String name, final String shortName, final S super(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, accountingRuleType, charges, allowOverdraft, overdraftLimit, - minBalanceForInterestCalculation, withHoldTax, taxGroup); + minBalanceForInterestCalculation, withHoldTax, taxGroup, null); if (charts != null) { this.charts = charts; diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 5db6e32dc14..a987560adba 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -71,6 +71,8 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import lombok.Getter; +import lombok.Setter; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; @@ -117,17 +119,18 @@ import org.apache.fineract.portfolio.savings.exception.SavingsActivityPriorToClientTransferException; import org.apache.fineract.portfolio.savings.exception.SavingsOfficerAssignmentDateException; import org.apache.fineract.portfolio.savings.exception.SavingsOfficerUnassignmentDateException; +import org.apache.fineract.portfolio.savings.exception.SavingsTransferTransactionsAlreadyUndoneException; import org.apache.fineract.portfolio.savings.exception.SavingsTransferTransactionsCannotBeUndoneException; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.portfolio.tax.domain.TaxComponent; import org.apache.fineract.portfolio.tax.domain.TaxGroup; import org.apache.fineract.portfolio.tax.service.TaxUtils; import org.apache.fineract.useradministration.domain.AppUser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; @Entity +@Getter +@Setter @Table(name = "m_savings_account", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "sa_account_no_UNIQUE"), @UniqueConstraint(columnNames = { "external_id" }, name = "sa_external_id_UNIQUE") }) @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @@ -136,8 +139,6 @@ @SuppressWarnings({ "MemberName" }) public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom { - private static final Logger LOG = LoggerFactory.getLogger(SavingsAccount.class); - @Version int version; @@ -337,12 +338,16 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom @JoinColumn(name = "tax_group_id") private TaxGroup taxGroup; + @Column(name = "accrued_till_date") + protected LocalDate accruedTillDate; + @Column(name = "total_savings_amount_on_hold", scale = 6, precision = 19, nullable = true) private BigDecimal savingsOnHoldAmount; @OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY) protected List identifiers = new ArrayList<>(); public transient ConfigurationDomainService configurationDomainService; + public transient SavingsAccountTransaction newTransaction; protected SavingsAccount() { // @@ -501,10 +506,6 @@ public boolean isClosed() { return SavingsAccountStatusType.fromInt(this.status).isClosed(); } - public List getIdentifiers() { - return identifiers; - } - public void postInterest(final MathContext mc, final LocalDate interestPostingUpToDate, final boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { @@ -642,6 +643,10 @@ public void postInterest(final MathContext mc, final LocalDate interestPostingUp } } + public List getIdentifiers() { + return identifiers; + } + protected List findWithHoldTransactions() { final List withholdTransactions = new ArrayList<>(); List trans = getTransactions(); @@ -664,7 +669,7 @@ protected List findWithHoldSavingsTransactionsWithPiv return withholdTransactions; } - private boolean isWithHoldTaxApplicableForInterestPosting() { + public boolean isWithHoldTaxApplicableForInterestPosting() { return this.withHoldTax() && this.depositAccountType().isSavingsDeposit(); } @@ -846,6 +851,7 @@ public List calculateInterestUsing(final MathContext mc, final Lo if (postInterestOnDate != null) { postedAsOnDates.add(postInterestOnDate); } + final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth, postedAsOnDates); @@ -926,8 +932,7 @@ private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc) { return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100L), mc); } - @SuppressWarnings("unused") - protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) { + public BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) { return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc); } @@ -939,6 +944,20 @@ private boolean hasOverdraftInterestCalculation() { return isAllowOverdraft() && !MathUtil.isEmpty(getOverdraftLimit()) && !MathUtil.isEmpty(nominalAnnualInterestRateOverdraft); } + public List retreiveOrderedAccrualTransactions() { + final List listOfTransactionsSorted = retrieveListOfTransactions(); + + final List orderedAccrualTransactions = new ArrayList<>(); + + for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { + if (transaction.isAccrual()) { + orderedAccrualTransactions.add(transaction); + } + } + orderedAccrualTransactions.sort(new SavingsAccountTransactionComparator()); + return orderedAccrualTransactions; + } + protected List retreiveOrderedNonInterestPostingTransactions() { final List listOfTransactionsSorted = retrieveListOfTransactions(); @@ -946,7 +965,7 @@ protected List retreiveOrderedNonInterestPostingTrans for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) - && transaction.isNotReversed() && !transaction.isReversalTransaction()) { + && transaction.isNotReversed() && !transaction.isReversalTransaction() && !transaction.isAccrual()) { orderedNonInterestPostingTransactions.add(transaction); } } @@ -989,6 +1008,7 @@ protected List retrieveListOfTransactions() { protected void recalculateDailyBalances(final Money openingAccountBalance, final LocalDate interestPostingUpToDate, final boolean backdatedTxnsAllowedTill, boolean postReversals) { Money runningBalance = openingAccountBalance; + BigDecimal previewBalance = BigDecimal.ZERO; boolean calculateInterest = hasInterestCalculation() || hasOverdraftInterestCalculation(); List accountTransactionsSorted = null; @@ -1001,7 +1021,9 @@ protected void recalculateDailyBalances(final Money openingAccountBalance, final boolean isTransactionsModified = false; for (final SavingsAccountTransaction transaction : accountTransactionsSorted) { + boolean typeTransaccionValidation = transaction.getTransactionType() == SavingsAccountTransactionType.ACCRUAL; if (transaction.isReversed() || transaction.isReversalTransaction()) { + transaction.setNegativeBalance(MathUtil.isLessThanZero(transaction.getRunningBalance())); transaction.zeroBalanceFields(); } else { Money overdraftAmount = Money.zero(this.currency); @@ -1022,6 +1044,12 @@ protected void recalculateDailyBalances(final Money openingAccountBalance, final } transactionAmount = transactionAmount.minus(transaction.getAmount(this.currency)); } + if (typeTransaccionValidation && this.newTransaction != null + && (transaction.getDateOf().isAfter(this.newTransaction.getDateOf()) + || transaction.getDateOf().isEqual(this.newTransaction.getDateOf()))) { + transaction.setNegativeBalance(MathUtil.isLessThanZero(transaction.getRunningBalance())); + transaction.reverse(); + } runningBalance = runningBalance.plus(transactionAmount); transaction.setRunningBalance(runningBalance); @@ -1029,7 +1057,7 @@ protected void recalculateDailyBalances(final Money openingAccountBalance, final if (MathUtil.isEmpty(overdraftAmount) && runningBalance.isLessThanZero() && !transaction.isAmountOnHold()) { overdraftAmount = runningBalance.negated(); } - if (!calculateInterest || transaction.getId() == null) { + if (!calculateInterest || transaction.getId() == null || transaction.getOverdraftAmount(this.currency).isZero()) { transaction.setOverdraftAmount(overdraftAmount); } else if (!MathUtil.isEqualTo(overdraftAmount, transaction.getOverdraftAmount(this.currency))) { SavingsAccountTransaction accountTransaction = SavingsAccountTransaction.copyTransaction(transaction); @@ -1110,15 +1138,17 @@ public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO tran final Long relaxingDaysConfigForPivotDate, final String refNo) { final String resourceTypeName = depositAccountType().resourceName(); if (isNotActive()) { - final String defaultUserMessage = "Transaction is not allowed. Account is not active."; - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg." + resourceTypeName + ".transaction.account.is.not.active", defaultUserMessage, "transactionDate", - transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); + if (!SavingsAccountStatusType.fromInt(this.status).isMatured()) { + final String defaultUserMessage = "Transaction is not allowed. Account is not active."; + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg." + resourceTypeName + ".transaction.account.is.not.active", defaultUserMessage, "transactionDate", + transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); - throw new PlatformApiDataValidationException(dataValidationErrors); + throw new PlatformApiDataValidationException(dataValidationErrors); + } } if (DateUtils.isDateInTheFuture(transactionDTO.getTransactionDate())) { @@ -1158,6 +1188,7 @@ public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO tran if (backdatedTxnsAllowedTill) { addTransactionToExisting(transaction); } else { + addTransactionNew(transaction); addTransaction(transaction); } @@ -1293,6 +1324,7 @@ public SavingsAccountTransaction withdraw(final SavingsAccountTransactionDTO tra if (backdatedTxnsAllowedTill) { addTransactionToExisting(transaction); } else { + addTransactionNew(transaction); addTransaction(transaction); } @@ -1976,11 +2008,15 @@ public SavingsProduct savingsProduct() { return this.product; } - private Boolean isCashBasedAccountingEnabledOnSavingsProduct() { + public Boolean isCashBasedAccountingEnabledOnSavingsProduct() { return this.product.isCashBasedAccountingEnabled(); } - private Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() { + public Boolean isPeriodicAccrualAccounting() { + return this.product.isPeriodicAccrualAccounting(); + } + + public Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() { return this.product.isAccrualBasedAccountingEnabled(); } @@ -2375,6 +2411,50 @@ public void undoTransaction(final Long transactionId) { } } + protected Map undoActivate() { + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.undoActivateAction); + + final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.active.state"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final LocalDate businessDate = DateUtils.getBusinessLocalDate(); + + this.status = SavingsAccountStatusType.APPROVED.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = null; + this.closedBy = null; + this.activatedOnDate = null; + this.activatedBy = null; + this.lockedInUntilDate = null; + + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNOD_ACTIVATE, businessDate); + + // Undo Transactions + for (SavingsAccountTransaction transaction : getTransactions()) { + if (!transaction.isReversed()) { + undoTransaction(transaction); + } + } + + return actualChanges; + } + public void undoSavingsTransaction(final Long transactionId) { SavingsAccountTransaction transactionToUndo = null; @@ -2385,7 +2465,7 @@ public void undoSavingsTransaction(final Long transactionId) { } if (transactionToUndo == null) { - throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionId); + throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionToUndo.getId()); } validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); @@ -2408,7 +2488,7 @@ public void undoSavingsTransaction(final Long transactionId) { public void undoTransaction(final SavingsAccountTransaction transactionToUndo) { if (transactionToUndo.isReversed()) { - throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionToUndo.getId()); + throw new SavingsTransferTransactionsAlreadyUndoneException(getAccountNumber(), transactionToUndo.getId()); } validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); @@ -2680,6 +2760,98 @@ public Map activate(final AppUser currentUser, final JsonCommand return actualChanges; } + protected Map undoActivate(final AppUser currentUser, final JsonCommand command, final LocalDate operationDate) { + + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.activateAction); + + final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.active.state"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.status = SavingsAccountStatusType.APPROVED.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = null; + this.closedBy = null; + this.activatedOnDate = null; + this.activatedBy = null; + this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationDate()); + + if (this.client != null && this.client.isActivatedAfter(operationDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(this.client.getActivationDate()); + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (this.group != null && this.group.isActivatedAfter(operationDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(this.client.getActivationDate()); + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.group.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final LocalDate approvalDate = getApprovedOnDate(); + if (operationDate.isBefore(approvalDate)) { + + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(approvalDate); + + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.approval.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_ACTIVATE, operationDate); + + updateSavingsToApprovedState(); + + return actualChanges; + } + + protected void updateSavingsToApprovedState() { + reverseExistingTransactions(); + + for (SavingsAccountCharge charge : this.charges()) { + charge.resetToOriginal(currency); + } + } + + protected void reverseExistingTransactions() { + Collection retainTransactions = new ArrayList<>(); + for (final SavingsAccountTransaction transaction : this.transactions) { + transaction.reverse(); + if (transaction.getId() != null) { + retainTransactions.add(transaction); + } + } + this.transactions.retainAll(retainTransactions); + } + public void processAccountUponActivation(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { // update annual fee due date @@ -2842,7 +3014,6 @@ private LocalDate calculateDateAccountIsLockedUntil(final LocalDate activationLo lockedInUntilLocalDate = activationLocalDate.plusYears(this.lockinPeriodFrequency); break; case WHOLE_TERM: - LOG.error("TODO Implement calculateDateAccountIsLockedUntil for WHOLE_TERM"); break; } @@ -2883,6 +3054,10 @@ public void addTransaction(final SavingsAccountTransaction transaction) { this.transactions.add(transaction); } + public void addTransactionNew(final SavingsAccountTransaction transaction) { + this.newTransaction = transaction; + } + public void addTransactionToExisting(final SavingsAccountTransaction transaction) { this.savingsAccountTransactions.add(transaction); } @@ -3205,6 +3380,38 @@ private void handleChargeTransactions(final SavingsAccountCharge savingsAccountC } else { this.transactions.add(transaction); } + + // Charge Accrual Recognition + final SavingsAccountTransaction savingsAccountAccrualTransaction = handleAccruedChargeAppliedTransaction( + transaction.getTransactionDate(), savingsAccountCharge); + if (savingsAccountAccrualTransaction != null) { + savingsAccountAccrualTransaction.getSavingsAccountChargesPaid().add(chargePaidBy); + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactions.add(savingsAccountAccrualTransaction); + } else { + this.transactions.add(savingsAccountAccrualTransaction); + } + } + } + + private SavingsAccountTransaction handleAccruedChargeAppliedTransaction(final LocalDate transactionDate, + final SavingsAccountCharge savingsAccountCharge) { + SavingsAccountTransaction savingsAccountAccrualTransaction = null; + if (isPeriodicAccrualAccounting()) { + if (isChargeToBeRecognizedAsAccrual(savingsAccountCharge)) { + savingsAccountAccrualTransaction = SavingsAccountTransaction.accrual(this, office(), transactionDate, + savingsAccountCharge.getAmount(getCurrency()), false, false); + } + } + return savingsAccountAccrualTransaction; + } + + private boolean isChargeToBeRecognizedAsAccrual(final SavingsAccountCharge savingsAccountCharge) { + final Collection chargeIds = savingsProduct().accrualChargeIds(); + if (chargeIds.isEmpty()) { + return false; + } + return chargeIds.contains(savingsAccountCharge.getCharge().getId()); } private SavingsAccountCharge getCharge(final Long savingsAccountChargeId) { @@ -3446,7 +3653,7 @@ protected boolean applyWithholdTaxForDepositAccounts(final LocalDate interestPos if (withholdTransaction == null && this.withHoldTax()) { boolean isWithholdTaxAdded = createWithHoldTransaction(totalInterestPosted, interestPostingUpToDate, backdatedTxnsAllowedTill); recalucateDailyBalance = recalucateDailyBalance || isWithholdTaxAdded; - } else { + } else if (withholdTransaction != null) { boolean isWithholdTaxAdded = updateWithHoldTransaction(totalInterestPosted, withholdTransaction); recalucateDailyBalance = recalucateDailyBalance || isWithholdTaxAdded; } @@ -3847,4 +4054,11 @@ public List toSavingsAccountTr .map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft)) .toList(); } + + public List toSavingsAccountTransactionDetailsForPostingPeriodList() { + return retreiveOrderedNonInterestPostingTransactions().stream() + .map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft)) + .toList(); + } + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java index 96aef542849..be7f8756902 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java @@ -28,6 +28,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import jakarta.persistence.Transient; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -41,8 +42,10 @@ import java.util.Optional; import java.util.Set; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.office.domain.Office; @@ -138,11 +141,14 @@ public final class SavingsAccountTransaction extends AbstractAuditableWithUTCDat @Column(name = "ref_no", nullable = true) private String refNo; + @Transient + private Boolean isNegativeBalance; + SavingsAccountTransaction() {} private SavingsAccountTransaction(final SavingsAccount savingsAccount, final Office office, final PaymentDetail paymentDetail, final Integer typeOf, final LocalDate transactionLocalDate, final BigDecimal amount, final boolean isReversed, - final boolean isManualTransaction, final Boolean lienTransaction, final String refNo) { + final boolean isManualTransaction, final Boolean lienTransaction, final String refNo, final Boolean isNegativeBalance) { this.savingsAccount = savingsAccount; this.office = office; this.typeOf = typeOf; @@ -155,19 +161,21 @@ private SavingsAccountTransaction(final SavingsAccount savingsAccount, final Off this.isManualTransaction = isManualTransaction; this.lienTransaction = lienTransaction; this.refNo = refNo; + this.isNegativeBalance = isNegativeBalance; } private SavingsAccountTransaction(final SavingsAccount savingsAccount, final Office office, final Integer typeOf, final LocalDate transactionLocalDate, final Money amount, final boolean isReversed, final boolean isManualTransaction, - final Boolean lienTransaction, final String refNo) { - this(savingsAccount, office, null, typeOf, transactionLocalDate, amount, isReversed, isManualTransaction, lienTransaction, refNo); + final Boolean lienTransaction, final String refNo, final Boolean isNegativeBalance) { + this(savingsAccount, office, null, typeOf, transactionLocalDate, amount, isReversed, isManualTransaction, lienTransaction, refNo, + isNegativeBalance); } private SavingsAccountTransaction(final SavingsAccount savingsAccount, final Office office, final PaymentDetail paymentDetail, final Integer typeOf, final LocalDate transactionLocalDate, final Money amount, final boolean isReversed, - final boolean isManualTransaction, final Boolean lienTransaction, final String refNo) { + final boolean isManualTransaction, final Boolean lienTransaction, final String refNo, final Boolean isNegativeBalance) { this(savingsAccount, office, paymentDetail, typeOf, transactionLocalDate, amount.getAmount(), isReversed, isManualTransaction, - lienTransaction, refNo); + lienTransaction, refNo, isNegativeBalance); } public static SavingsAccountTransaction deposit(final SavingsAccount savingsAccount, final Office office, @@ -176,7 +184,7 @@ public static SavingsAccountTransaction deposit(final SavingsAccount savingsAcco final boolean isManualTransaction = false; final Boolean lienTransaction = false; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.DEPOSIT.getValue(), date, - amount, isReversed, isManualTransaction, lienTransaction, refNo); + amount, isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction deposit(final SavingsAccount savingsAccount, final Office office, @@ -186,7 +194,7 @@ public static SavingsAccountTransaction deposit(final SavingsAccount savingsAcco final boolean isManualTransaction = false; final Boolean lienTransaction = false; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, savingsAccountTransactionType.getValue(), date, amount, - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction withdrawal(final SavingsAccount savingsAccount, final Office office, @@ -195,7 +203,16 @@ public static SavingsAccountTransaction withdrawal(final SavingsAccount savingsA final boolean isManualTransaction = false; final Boolean lienTransaction = false; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.WITHDRAWAL.getValue(), - date, amount, isReversed, isManualTransaction, lienTransaction, refNo); + date, amount, isReversed, isManualTransaction, lienTransaction, refNo, false); + } + + public static SavingsAccountTransaction accrual(final SavingsAccount savingsAccount, final Office office, final LocalDate date, + final Money amount, final boolean isManualTransaction, final Boolean isNegativeBalance) { + final boolean isReversed = false; + final Boolean lienTransaction = false; + final String refNo = ExternalId.generate().getValue(); + return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.ACCRUAL.getValue(), date, amount, + isReversed, isManualTransaction, lienTransaction, refNo, isNegativeBalance); } public static SavingsAccountTransaction interestPosting(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -204,7 +221,7 @@ public static SavingsAccountTransaction interestPosting(final SavingsAccount sav final Boolean lienTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.INTEREST_POSTING.getValue(), date, - amount, isReversed, isManualTransaction, lienTransaction, refNo); + amount, isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction overdraftInterest(final SavingsAccount savingsAccount, final Office office, @@ -213,7 +230,7 @@ public static SavingsAccountTransaction overdraftInterest(final SavingsAccount s final Boolean lienTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.OVERDRAFT_INTEREST.getValue(), date, - amount, isReversed, isManualTransaction, lienTransaction, refNo); + amount, isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction withdrawalFee(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -222,7 +239,7 @@ public static SavingsAccountTransaction withdrawalFee(final SavingsAccount savin final boolean isManualTransaction = false; final Boolean lienTransaction = false; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.WITHDRAWAL_FEE.getValue(), date, amount, - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction annualFee(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -232,7 +249,7 @@ public static SavingsAccountTransaction annualFee(final SavingsAccount savingsAc final Boolean lienTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.ANNUAL_FEE.getValue(), date, amount, - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction charge(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -242,7 +259,7 @@ public static SavingsAccountTransaction charge(final SavingsAccount savingsAccou final Boolean lienTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.PAY_CHARGE.getValue(), date, amount, - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction waiver(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -252,7 +269,7 @@ public static SavingsAccountTransaction waiver(final SavingsAccount savingsAccou final Boolean lienTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.WAIVE_CHARGES.getValue(), date, amount, - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction initiateTransfer(final SavingsAccount savingsAccount, final Office office, @@ -264,7 +281,7 @@ public static SavingsAccountTransaction initiateTransfer(final SavingsAccount sa final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.INITIATE_TRANSFER.getValue(), date, savingsAccount.getSummary().getAccountBalance(), - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction approveTransfer(final SavingsAccount savingsAccount, final Office office, @@ -276,7 +293,7 @@ public static SavingsAccountTransaction approveTransfer(final SavingsAccount sav final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.APPROVE_TRANSFER.getValue(), date, savingsAccount.getSummary().getAccountBalance(), - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction withdrawTransfer(final SavingsAccount savingsAccount, final Office office, @@ -288,7 +305,7 @@ public static SavingsAccountTransaction withdrawTransfer(final SavingsAccount sa final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.WITHDRAW_TRANSFER.getValue(), date, savingsAccount.getSummary().getAccountBalance(), - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction withHoldTax(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -299,7 +316,7 @@ public static SavingsAccountTransaction withHoldTax(final SavingsAccount savings final String refNo = null; SavingsAccountTransaction accountTransaction = new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.WITHHOLD_TAX.getValue(), date, amount, isReversed, isManualTransaction, lienTransaction, - refNo); + refNo, false); updateTaxDetails(taxDetails, accountTransaction); return accountTransaction; } @@ -312,13 +329,13 @@ public static SavingsAccountTransaction escheat(final SavingsAccount savingsAcco final String refNo = null; return new SavingsAccountTransaction(savingsAccount, savingsAccount.office(), paymentDetail, SavingsAccountTransactionType.ESCHEAT.getValue(), date, savingsAccount.getSummary().getAccountBalance(), isReversed, - accountTransaction, lienTransaction, refNo); + accountTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction copyTransaction(SavingsAccountTransaction accountTransaction) { return new SavingsAccountTransaction(accountTransaction.savingsAccount, accountTransaction.office, accountTransaction.paymentDetail, accountTransaction.typeOf, accountTransaction.getTransactionDate(), accountTransaction.amount, accountTransaction.reversed, - accountTransaction.isManualTransaction, accountTransaction.lienTransaction, accountTransaction.refNo); + accountTransaction.isManualTransaction, accountTransaction.lienTransaction, accountTransaction.refNo, false); } public static SavingsAccountTransaction holdAmount(final SavingsAccount savingsAccount, final Office office, @@ -327,14 +344,14 @@ public static SavingsAccountTransaction holdAmount(final SavingsAccount savingsA final boolean isManualTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.AMOUNT_HOLD.getValue(), - date, amount, isReversed, isManualTransaction, lienTransaction, refNo); + date, amount, isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction releaseAmount(SavingsAccountTransaction accountTransaction, LocalDate transactionDate) { return new SavingsAccountTransaction(accountTransaction.savingsAccount, accountTransaction.office, accountTransaction.paymentDetail, SavingsAccountTransactionType.AMOUNT_RELEASE.getValue(), transactionDate, accountTransaction.amount, accountTransaction.reversed, accountTransaction.isManualTransaction, accountTransaction.lienTransaction, - accountTransaction.refNo); + accountTransaction.refNo, false); } public static SavingsAccountTransaction reversal(SavingsAccountTransaction accountTransaction) { @@ -559,6 +576,10 @@ public boolean isTransferRelatedTransaction() { return isTransferInitiation() || isTransferApproval() || isTransferRejection() || isTransferWithdrawal(); } + public boolean isAccrual() { + return getTransactionType().isAccrual(); + } + public void zeroBalanceFields() { this.runningBalance = null; this.cumulativeBalance = null; @@ -603,6 +624,8 @@ public Map toMapData(final String currencyCode) { thisTransactionData.put("currencyCode", currencyCode); thisTransactionData.put("amount", this.amount); thisTransactionData.put("overdraftAmount", this.overdraftAmount); + thisTransactionData.put("isNegativeBalance", + this.isNegativeBalance != null ? this.isNegativeBalance : MathUtil.isLessThanZero(runningBalance)); if (this.paymentDetail != null) { thisTransactionData.put("paymentTypeId", this.paymentDetail.getPaymentType().getId()); @@ -667,7 +690,7 @@ public EndOfDayBalance toEndOfDayBalance(final LocalDateInterval periodInterval, numberOfDays = newInterval.daysInPeriodInclusiveOfEndDate(); } - return EndOfDayBalance.from(balanceDate, openingBalance, endOfDayBalance, numberOfDays); + return EndOfDayBalance.from(balanceDate, openingBalance, endOfDayBalance, numberOfDays, currency.getDigitsAfterDecimal()); } public EndOfDayBalance toEndOfDayBalance(final Money openingBalance, final LocalDate nextTransactionDate) { @@ -683,7 +706,7 @@ public EndOfDayBalance toEndOfDayBalance(final Money openingBalance, final Local if (!openingBalance.isEqualTo(endOfDayBalance) && numberOfDays > 1) { numberOfDays = numberOfDays - 1; } - return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, numberOfDays); + return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, numberOfDays, currency.getDigitsAfterDecimal()); } public EndOfDayBalance toEndOfDayBalance(final Money openingBalance) { @@ -700,7 +723,8 @@ public EndOfDayBalance toEndOfDayBalance(final Money openingBalance) { } } - return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays); + return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays, + currency.getDigitsAfterDecimal()); } public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, final LocalDateInterval boundedBy) { @@ -738,7 +762,8 @@ public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, fi numberOfDaysOfBalance = spanOfBalance.daysInPeriodInclusiveOfEndDate(); } - return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance); + return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance, + currency.getDigitsAfterDecimal()); } public boolean isBalanceInExistencesForOneDayOrMore() { @@ -881,4 +906,8 @@ public SavingsAccountTransactionDetailsForPostingPeriod toSavingsAccountTransact this.amount, currency, this.balanceNumberOfDays, isDeposit(), isWithdrawal(), isAllowOverDraft, isChargeTransactionAndNotReversed(), isDividendPayoutAndNotReversed()); } + + public void setNegativeBalance(Boolean negativeBalance) { + isNegativeBalance = negativeBalance; + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java index ca5779939ef..1191b278917 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java @@ -28,6 +28,8 @@ public enum SavingsEvent { SAVINGS_APPLICATION_APPROVED("application.approval"), // SAVINGS_APPLICATION_APPROVAL_UNDO("application.approval.undo"), // SAVINGS_ACTIVATE("activate"), // + SAVINGS_UNOD_ACTIVATE("activate.undo"), // + SAVINGS_UNDO_ACTIVATE("undo.activate"), // SAVINGS_DEPOSIT("deposit"), // SAVINGS_WITHDRAWAL("withdraw"), // SAVINGS_POST_INTEREST("interest.post"), // diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java index f958a7b5224..8e3ca453301 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java @@ -20,6 +20,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_PRODUCT_RESOURCE_NAME; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountingRuleParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accrualChargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.currencyCodeParamName; @@ -163,6 +164,10 @@ public class SavingsProduct extends AbstractPersistableCustom { @JoinTable(name = "m_savings_product_charge", joinColumns = @JoinColumn(name = "savings_product_id"), inverseJoinColumns = @JoinColumn(name = "charge_id")) protected Set charges; + @ManyToMany + @JoinTable(name = "m_savings_product_accrual_charge", joinColumns = @JoinColumn(name = "savings_product_id"), inverseJoinColumns = @JoinColumn(name = "charge_id")) + protected Set accrualCharges; + @Column(name = "allow_overdraft") private boolean allowOverdraft; @@ -220,14 +225,15 @@ public static SavingsProduct createNew(final String name, final String shortName final BigDecimal minRequiredBalance, final boolean lienAllowed, final BigDecimal maxAllowedLienLimit, final BigDecimal minBalanceForInterestCalculation, final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation, boolean withHoldTax, TaxGroup taxGroup, - final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat) { + final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat, + final Set accrualCharges) { return new SavingsProduct(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, accountingRuleType, charges, allowOverdraft, overdraftLimit, enforceMinRequiredBalance, minRequiredBalance, lienAllowed, maxAllowedLienLimit, minBalanceForInterestCalculation, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, - taxGroup, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat); + taxGroup, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accrualCharges); } protected SavingsProduct() { @@ -242,11 +248,12 @@ protected SavingsProduct(final String name, final String shortName, final String final Integer lockinPeriodFrequency, final SavingsPeriodFrequencyType lockinPeriodFrequencyType, final boolean withdrawalFeeApplicableForTransfer, final AccountingRuleType accountingRuleType, final Set charges, final boolean allowOverdraft, final BigDecimal overdraftLimit, BigDecimal minBalanceForInterestCalculation, boolean withHoldTax, - TaxGroup taxGroup) { + TaxGroup taxGroup, final Set accrualCharges) { this(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, accountingRuleType, charges, allowOverdraft, overdraftLimit, - false, null, false, null, minBalanceForInterestCalculation, null, null, withHoldTax, taxGroup, null, null, null, null); + false, null, false, null, minBalanceForInterestCalculation, null, null, withHoldTax, taxGroup, null, null, null, null, + accrualCharges); } protected SavingsProduct(final String name, final String shortName, final String description, final MonetaryCurrency currency, @@ -259,7 +266,8 @@ protected SavingsProduct(final String name, final String shortName, final String final BigDecimal minRequiredBalance, final boolean lienAllowed, final BigDecimal maxAllowedLienLimit, BigDecimal minBalanceForInterestCalculation, final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation, final boolean withHoldTax, final TaxGroup taxGroup, - final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat) { + final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat, + final Set accrualCharges) { this.name = name; this.shortName = shortName; @@ -291,6 +299,10 @@ protected SavingsProduct(final String name, final String shortName, final String this.charges = charges; } + if (accrualCharges != null) { + this.accrualCharges = accrualCharges; + } + validateLockinDetails(); this.allowOverdraft = allowOverdraft; this.overdraftLimit = overdraftLimit; @@ -474,7 +486,6 @@ public Map update(final JsonCommand command) { this.lockinPeriodFrequencyType = newValue != null ? SavingsPeriodFrequencyType.fromInt(newValue).getValue() : newValue; } - // set period type to null if frequency is null if (this.lockinPeriodFrequency == null) { this.lockinPeriodFrequencyType = null; } @@ -491,12 +502,18 @@ public Map update(final JsonCommand command) { this.accountingRule = newValue; } - // charges if (command.hasParameter(chargesParamName)) { - final JsonArray jsonArray = command.arrayOfParameterNamed(chargesParamName); + JsonArray jsonArray = command.arrayOfParameterNamed(chargesParamName); if (jsonArray != null) { actualChanges.put(chargesParamName, command.jsonFragment(chargesParamName)); } + + if (command.hasParameter(accrualChargesParamName)) { + jsonArray = command.arrayOfParameterNamed(accrualChargesParamName); + if (jsonArray != null) { + actualChanges.put(accrualChargesParamName, command.jsonFragment(accrualChargesParamName)); + } + } } if (command.isChangeInBooleanParameterNamed(allowOverdraftParamName, this.allowOverdraft)) { @@ -653,7 +670,6 @@ public boolean isCashBasedAccountingEnabled() { } // TODO this entire block is currently unnecessary as Savings does not have - // accrual accounting public boolean isAccrualBasedAccountingEnabled() { return isUpfrontAccrualAccounting() || isPeriodicAccrualAccounting(); } @@ -723,6 +739,14 @@ public Set charges() { return this.charges; } + public Set accrualCharges() { + return this.accrualCharges; + } + + public List accrualChargeIds() { + return accrualCharges.stream().map(Charge::getId).toList(); + } + public InterestRateChart applicableChart(@SuppressWarnings("unused") final LocalDate target) { return null; } @@ -779,4 +803,12 @@ public Long getDaysToEscheat() { return this.daysToEscheat; } + public void setCharges(Set charges) { + this.charges = charges; + } + + public void setAccrualCharges(Set accrualCharges) { + this.accrualCharges = accrualCharges; + } + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java index c47cb76ee3a..55db0d5d731 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java @@ -18,38 +18,7 @@ */ package org.apache.fineract.portfolio.savings.domain; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.currencyCodeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.daysToDormancyParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.daysToEscheatParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.daysToInactiveParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.descriptionParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.digitsAfterDecimalParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.idParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.inMultiplesOfParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationDaysInYearTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCompoundingPeriodTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestPostingPeriodTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.isDormancyTrackingActiveParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.maxAllowedLienLimitParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minBalanceForInterestCalculationParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minOverdraftForInterestCalculationParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minRequiredBalanceParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minRequiredOpeningBalanceParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nameParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateOverdraftParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shortNameParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawalFeeForTransfersParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.*; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -142,6 +111,7 @@ public SavingsProduct assemble(final JsonCommand command) { // Savings product charges final Set charges = assembleListOfSavingsProductCharges(command, currencyCode); + final Set accrualCharges = assembleListOfSavingsProductCharges(command, currencyCode); boolean allowOverdraft = false; if (command.parameterExists(allowOverdraftParamName)) { @@ -198,7 +168,7 @@ public SavingsProduct assemble(final JsonCommand command) { lockinPeriodFrequency, lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, accountingRuleType, charges, allowOverdraft, overdraftLimit, enforceMinRequiredBalance, minRequiredBalance, lienAllowed, maxAllowedLienLimit, minBalanceForInterestCalculation, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, - taxGroup, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat); + taxGroup, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accrualCharges); } public Set assembleListOfSavingsProductCharges(final JsonCommand command, final String savingsProductCurrencyCode) { diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountBlockedException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountBlockedException.java index 50f7d4103bd..d934705187c 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountBlockedException.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountBlockedException.java @@ -26,4 +26,9 @@ public SavingsAccountBlockedException(final Long accountId) { super("error.msg.saving.account.blocked.transaction.not.allowed", "Any transaction to " + accountId + " is not allowed, since the account is blocked", accountId); } + + public SavingsAccountBlockedException(final String accountNo) { + super("error.msg.saving.account.blocked.transaction.not.allowed", + "Any transaction to " + accountNo + " is not allowed, since the account is blocked", accountNo); + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountCreditsBlockedException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountCreditsBlockedException.java index d54cb87745f..c4e1647925a 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountCreditsBlockedException.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountCreditsBlockedException.java @@ -26,4 +26,9 @@ public SavingsAccountCreditsBlockedException(final Long accountId) { super("error.msg.savings.account.credit.transaction.not.allowed", "Any Credit transactions to " + accountId + " is not allowed, since the account is blocked for credits", accountId); } + + public SavingsAccountCreditsBlockedException(final String accountNo) { + super("error.msg.savings.account.credit.transaction.not.allowed", + "Any Credit transactions to " + accountNo + " is not allowed, since the account is blocked for credits", accountNo); + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountDebitsBlockedException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountDebitsBlockedException.java index 8a2903b4ddf..412c547eae4 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountDebitsBlockedException.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountDebitsBlockedException.java @@ -27,4 +27,8 @@ public SavingsAccountDebitsBlockedException(final Long accountId) { "Any debit transactions from " + accountId + " is not allowed, since the account is blocked for debits", accountId); } + public SavingsAccountDebitsBlockedException(final String accountNo) { + super("error.msg.savings.account.debit.transaction.not.allowed", + "Any debit transactions from " + accountNo + " is not allowed, since the account is blocked for debits", accountNo); + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsDepositsWithActiveTransferFundsException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsDepositsWithActiveTransferFundsException.java new file mode 100644 index 00000000000..8769c72ad72 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsDepositsWithActiveTransferFundsException.java @@ -0,0 +1,35 @@ +/** + * 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 org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * {@link AbstractPlatformDomainRuleException} thrown an action to transition a loan from one state to another violates + * a domain rule. + */ +public class SavingsDepositsWithActiveTransferFundsException extends AbstractPlatformDomainRuleException { + + public SavingsDepositsWithActiveTransferFundsException(final String accountNo, final Long transactionId) { + super("error.msg.savings.deposits.with.active.transfer.funds.transaction", + "Savings/Deposit account " + accountNo + " with active transfer funds transaction " + transactionId, transactionId, + accountNo); + } + +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsTransferTransactionsAlreadyUndoneException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsTransferTransactionsAlreadyUndoneException.java new file mode 100644 index 00000000000..526fafc17b5 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsTransferTransactionsAlreadyUndoneException.java @@ -0,0 +1,34 @@ +/** + * 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 org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * {@link AbstractPlatformDomainRuleException} thrown an action to transition a loan from one state to another violates + * a domain rule. + */ +public class SavingsTransferTransactionsAlreadyUndoneException extends AbstractPlatformDomainRuleException { + + public SavingsTransferTransactionsAlreadyUndoneException(final String accountNo, final Long transactionId) { + super("error.msg.savings.transfer.transactions.cannot.be.undone", + "Transaction related to savings account " + accountNo + " already undone", transactionId, accountNo); + } + +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java index 325c9dd8a63..c3990f949e0 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.savings.service; import java.math.BigDecimal; +import java.math.MathContext; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; @@ -39,7 +40,7 @@ SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatte boolean backdatedTxnsAllowedTill); void postJournalEntries(SavingsAccount savingsAccount, Set existingTransactionIds, Set existingReversedTransactionIds, - boolean backdatedTxnsAllowedTill); + boolean backdatedTxnsAllowedTill, boolean isNegativeBalance); SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount, boolean backdatedTxnsAllowedTill); @@ -48,4 +49,15 @@ SavingsAccountTransaction handleReversal(SavingsAccount account, List retrieveAllSavingsDataForInterestPosting(boolean backda List retrieveAllTransactionData(List refNo); Long retrieveAccountIdByExternalId(ExternalId externalId); + + Collection retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings); + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java index 295d3f55aee..fe5a35417f2 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java @@ -26,16 +26,13 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.UUID; +import java.util.*; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.savings.data.SavingsAccountData; @@ -108,33 +105,44 @@ private void batchUpdateJournalEntries(final List savingsAcc for (SavingsAccountTransactionData savingsAccountTransactionData : savingsAccountTransactionDataList) { if (savingsAccountTransactionData.getId() == null) { final String key = savingsAccountTransactionData.getRefNo(); - if (savingsAccountTransactionDataHashMap.containsKey(key)) { - final SavingsAccountTransactionData dataFromFetch = savingsAccountTransactionDataHashMap.get(key); - savingsAccountTransactionData.setId(dataFromFetch.getId()); - if (savingsAccountData.getGlAccountIdForSavingsControl() != 0 - && savingsAccountData.getGlAccountIdForInterestOnSavings() != 0) { - OffsetDateTime auditDatetime = DateUtils.getAuditOffsetDateTime(); - paramsForGLInsertion.add(new Object[] { savingsAccountData.getGlAccountIdForSavingsControl(), - savingsAccountData.getOfficeId(), null, currencyCode, - SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(), - savingsAccountTransactionData.getId(), null, false, null, false, - savingsAccountTransactionData.getTransactionDate(), JournalEntryType.CREDIT.getValue().longValue(), - savingsAccountTransactionData.getAmount(), null, JournalEntryType.CREDIT.getValue().longValue(), - savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO, null, - savingsAccountTransactionData.getTransactionDate(), null, userId, userId, - DateUtils.getBusinessLocalDate() }); - - paramsForGLInsertion.add(new Object[] { savingsAccountData.getGlAccountIdForInterestOnSavings(), - savingsAccountData.getOfficeId(), null, currencyCode, - SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(), - savingsAccountTransactionData.getId(), null, false, null, false, - savingsAccountTransactionData.getTransactionDate(), JournalEntryType.DEBIT.getValue().longValue(), - savingsAccountTransactionData.getAmount(), null, JournalEntryType.DEBIT.getValue().longValue(), - savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO, null, - savingsAccountTransactionData.getTransactionDate(), null, userId, userId, - DateUtils.getBusinessLocalDate() }); - } + final Boolean isNegativeBalance = savingsAccountTransactionData.getIsNegativeBalance(); + final SavingsAccountTransactionData dataFromFetch = savingsAccountTransactionDataHashMap.get(key); + savingsAccountTransactionData.setId(dataFromFetch.getId()); + if (savingsAccountData.getGlAccountIdForSavingsControl() != 0 + && savingsAccountData.getGlAccountIdForInterestOnSavings() != 0) { + OffsetDateTime auditDatetime = DateUtils.getAuditOffsetDateTime(); + paramsForGLInsertion.add(new Object[] { + isNegativeBalance + ? MathUtil.isLessThanZero(savingsAccountTransactionData.getRunningBalance()) + ? savingsAccountData.getGlAccountIdForInterestReceivableNegative() + : savingsAccountData.getGlAccountIdForInterestReceivableNegative() + : savingsAccountData.getGlAccountIdForSavingsControl(), + + savingsAccountData.getOfficeId(), null, currencyCode, + SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(), + savingsAccountTransactionData.getId(), null, false, null, false, + savingsAccountTransactionData.getTransactionDate(), JournalEntryType.CREDIT.getValue().longValue(), + savingsAccountTransactionData.getAmount(), null, JournalEntryType.CREDIT.getValue().longValue(), + savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO, null, + savingsAccountTransactionData.getTransactionDate(), null, userId, userId, + DateUtils.getBusinessLocalDate() }); + + paramsForGLInsertion.add(new Object[] { + isNegativeBalance + ? MathUtil.isLessThanZero(savingsAccountTransactionData.getRunningBalance()) + ? savingsAccountData.getGlAccountIdForOverdraftPorfolioNegative() + : savingsAccountData.getGlAccountIdForSavingsControlAcountPositiveInterestNegative() + : savingsAccountData.getGlAccountIdForInterestOnSavings(), + savingsAccountData.getOfficeId(), null, currencyCode, + SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(), + savingsAccountTransactionData.getId(), null, false, null, false, + savingsAccountTransactionData.getTransactionDate(), JournalEntryType.DEBIT.getValue().longValue(), + savingsAccountTransactionData.getAmount(), null, JournalEntryType.DEBIT.getValue().longValue(), + savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO, null, + savingsAccountTransactionData.getTransactionDate(), null, userId, userId, + DateUtils.getBusinessLocalDate() }); } + } } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java index 47a80859b34..7b4a50bde8c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java @@ -642,6 +642,15 @@ public static Integer createSavingsProduct(final String minOpenningBalance, fina return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); } + public static Integer createSavingsProductWithAccrualAccounting(final String minOpenningBalance, final Account... accounts) { + LOG.info("------------------------------CREATING NEW SAVINGS PRODUCT ---------------------------------------"); + final String savingsProductJSON = new SavingsProductHelper().withInterestCompoundingPeriodTypeAsDaily() // + .withInterestPostingPeriodTypeAsQuarterly() // + .withInterestCalculationPeriodTypeAsDailyBalance() // + .withMinimumOpenningBalance(minOpenningBalance).withAccountingRuleAsAccrualBased(accounts).build(); + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + private Integer createFixedDepositProduct(final String validFrom, final String validTo, Account... accounts) { LOG.info("------------------------------CREATING NEW FIXED DEPOSIT PRODUCT ---------------------------------------"); FixedDepositProductHelper fixedDepositProductHelper = new FixedDepositProductHelper(requestSpec, responseSpec);