From 6ee881b89a2e2b1648016213021408a8dd407bd0 Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Sat, 17 Feb 2024 21:07:23 -0500 Subject: [PATCH] FINERACT-1960: GL account mappings to use Savings product with Accrual accounting --- .../common/AccountingValidations.java | 46 +++ ...AccountMappingFromApiJsonDeserializer.java | 37 ++- ...AccountMappingReadPlatformServiceImpl.java | 151 +++++++--- ...avingsProductToGLAccountMappingHelper.java | 75 +++++ .../common/AccountingConstants.java | 58 +++- .../savings/DepositsApiConstants.java | 6 +- .../SavingsAccountTransactionType.java | 5 + .../SavingsAccountTransactionEnumData.java | 2 + .../savings/service/SavingsEnumerations.java | 4 + .../AccountingProcessorForSavingsFactory.java | 3 + .../service/AccountingProcessorHelper.java | 108 +++++++ ...ualBasedAccountingProcessorForSavings.java | 272 ++++++++++++++++++ ...ccountMappingWritePlatformServiceImpl.java | 125 ++++---- .../service/LoanScheduleAssembler.java | 5 +- .../LoanProductDataValidator.java | 25 +- ...ixedDepositProductsApiResourceSwagger.java | 101 ++----- ...ringDepositProductsApiResourceSwagger.java | 100 ++----- .../SavingsProductsApiResourceSwagger.java | 104 ++----- .../data/DepositProductDataValidator.java | 143 ++------- ...avingsAccountTransactionDataValidator.java | 13 +- ...DepositAccountReadPlatformServiceImpl.java | 1 - ...SavingsAccountReadPlatformServiceImpl.java | 84 +----- ...avingsDropdownReadPlatformServiceImpl.java | 6 +- fineract-savings/dependencies.gradle | 1 + ...SavingsProductAccountingDataValidator.java | 185 ++++++++++++ .../data/SavingsProductDataValidator.java | 229 ++------------- .../AccountingScenarioIntegrationTest.java | 119 +++++++- .../RecurringDepositProductHelper.java | 11 + .../common/savings/SavingsProductHelper.java | 55 ++++ 29 files changed, 1278 insertions(+), 796 deletions(-) create mode 100644 fineract-accounting/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java create mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductAccountingDataValidator.java diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.java new file mode 100644 index 000000000..aac47c5b6 --- /dev/null +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.java @@ -0,0 +1,46 @@ +/** + * 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 final class AccountingValidations { + + private 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-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 13e175dba..511ce5e80 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 @@ -31,7 +31,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; import org.apache.fineract.accounting.common.AccountingConstants.SavingProductAccountingParams; import org.apache.fineract.accounting.common.AccountingConstants.SharesProductAccountingParams; -import org.apache.fineract.accounting.common.AccountingRuleType; +import org.apache.fineract.accounting.common.AccountingValidations; import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; import org.apache.fineract.infrastructure.core.data.ApiParameterError; @@ -83,7 +83,8 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer { final Integer accountingRuleType = this.fromApiJsonHelper.extractIntegerNamed("accountingRule", element, Locale.getDefault()); baseDataValidator.reset().parameter("accountingRule").value(accountingRuleType).notNull().inMinMaxRange(1, 4); - if (isCashBasedAccounting(accountingRuleType) || isAccrualBasedAccounting(accountingRuleType)) { + if (AccountingValidations.isCashBasedAccounting(accountingRuleType) + || AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) { final Long fundAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.FUND_SOURCE.getValue(), element); baseDataValidator.reset().parameter(LoanProductAccountingParams.FUND_SOURCE.getValue()).value(fundAccountId).notNull() @@ -131,7 +132,7 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer { } - if (isAccrualBasedAccounting(accountingRuleType)) { + if (AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) { final Long receivableInterestAccountId = this.fromApiJsonHelper .extractLongNamed(LoanProductAccountingParams.INTEREST_RECEIVABLE.getValue(), element); @@ -168,7 +169,7 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer { Locale.getDefault()); baseDataValidator.reset().parameter(accountingRuleParamName).value(accountingRuleType).notNull().inMinMaxRange(1, 3); - if (isCashBasedAccounting(accountingRuleType)) { + if (AccountingValidations.isCashBasedAccounting(accountingRuleType)) { final Long savingsControlAccountId = this.fromApiJsonHelper .extractLongNamed(SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), element); @@ -227,6 +228,23 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer { } + // Periodic Accrual Accounting aditional GL Accounts + if (AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) { + final Long feeReceivableAccountId = this.fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.FEES_RECEIVABLE.getValue()).value(feeReceivableAccountId) + .notNull().integerGreaterThanZero(); + final Long penaltyReceivableAccountId = this.fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue()) + .value(penaltyReceivableAccountId).notNull().integerGreaterThanZero(); + + final Long interestPayableAccountId = this.fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.INTEREST_PAYABLE.getValue()).value(interestPayableAccountId) + .notNull().integerGreaterThanZero(); + } + throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -246,7 +264,7 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer { Locale.getDefault()); baseDataValidator.reset().parameter(accountingRuleParamName).value(accountingRuleType).notNull().inMinMaxRange(1, 3); - if (isCashBasedAccounting(accountingRuleType)) { + if (AccountingValidations.isCashBasedAccounting(accountingRuleType)) { final Long shareReferenceId = this.fromApiJsonHelper.extractLongNamed(SharesProductAccountingParams.SHARES_REFERENCE.getValue(), element); @@ -273,15 +291,6 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - private boolean isCashBasedAccounting(final Integer accountingRuleType) { - return AccountingRuleType.CASH_BASED.getValue().equals(accountingRuleType); - } - - private boolean isAccrualBasedAccounting(final Integer accountingRuleType) { - return AccountingRuleType.ACCRUAL_PERIODIC.getValue().equals(accountingRuleType) - || AccountingRuleType.ACCRUAL_UPFRONT.getValue().equals(accountingRuleType); - } - private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", 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 fa9bb087c..b9edf476d 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 @@ -25,7 +25,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan; +import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares; @@ -33,6 +35,7 @@ import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAcco import org.apache.fineract.accounting.common.AccountingConstants.SavingProductAccountingDataParams; import org.apache.fineract.accounting.common.AccountingConstants.SharesProductAccountingParams; import org.apache.fineract.accounting.common.AccountingRuleType; +import org.apache.fineract.accounting.common.AccountingValidations; import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; @@ -44,6 +47,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; +@Slf4j @Service @RequiredArgsConstructor public class ProductToGLAccountMappingReadPlatformServiceImpl implements ProductToGLAccountMappingReadPlatformService { @@ -106,7 +110,7 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product final List> listOfProductToGLAccountMaps = this.jdbcTemplate.query(sql, rm, // NOSONAR new Object[] { PortfolioProductType.LOAN.getValue(), loanProductId }); - if (AccountingRuleType.CASH_BASED.getValue().equals(accountingType)) { + if (AccountingValidations.isCashBasedAccounting(accountingType)) { for (final Map productToGLAccountMap : listOfProductToGLAccountMaps) { @@ -159,8 +163,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product } } - } else if (AccountingRuleType.ACCRUAL_UPFRONT.getValue().equals(accountingType) - || AccountingRuleType.ACCRUAL_PERIODIC.getValue().equals(accountingType)) { + } else if (AccountingValidations.isAccrualBasedAccounting(accountingType) + || AccountingValidations.isUpfrontAccrualAccounting(accountingType)) { for (final Map productToGLAccountMap : listOfProductToGLAccountMaps) { final Integer financialAccountType = (Integer) productToGLAccountMap.get("financialAccountType"); @@ -225,49 +229,21 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product @Override public Map fetchAccountMappingDetailsForSavingsProduct(final Long savingsProductId, final Integer accountingType) { - final Map accountMappingDetails = new LinkedHashMap<>(8); - final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null and mapping.charge_id is null "; final List> listOfProductToGLAccountMaps = this.jdbcTemplate.query(sql, rm, // NOSONAR new Object[] { PortfolioProductType.SAVING.getValue(), savingsProductId }); - if (AccountingRuleType.CASH_BASED.getValue().equals(accountingType)) { + Map accountMappingDetails = null; + if (AccountingValidations.isCashBasedAccounting(accountingType)) { + accountMappingDetails = setCashSavingsProductToGLAccountMaps(listOfProductToGLAccountMaps); - for (final Map productToGLAccountMap : listOfProductToGLAccountMaps) { + } else if (AccountingValidations.isAccrualPeriodicBasedAccounting(accountingType)) { + accountMappingDetails = setAccrualPeriodicSavingsProductToGLAccountMaps(listOfProductToGLAccountMaps); - final Integer financialAccountType = (Integer) productToGLAccountMap.get("financialAccountType"); - final CashAccountsForSavings glAccountForSavings = CashAccountsForSavings.fromInt(financialAccountType); - - final Long glAccountId = (Long) productToGLAccountMap.get("glAccountId"); - final String glAccountName = (String) productToGLAccountMap.get("glAccountName"); - final String glCode = (String) productToGLAccountMap.get("glCode"); - final GLAccountData gLAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); - - if (glAccountForSavings.equals(CashAccountsForSavings.SAVINGS_REFERENCE)) { - accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_REFERENCE.getValue(), gLAccountData); - } else if (glAccountForSavings.equals(CashAccountsForSavings.SAVINGS_CONTROL)) { - accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), gLAccountData); - } else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_FEES)) { - accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), gLAccountData); - } else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_PENALTIES)) { - accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_PENALTIES.getValue(), gLAccountData); - } else if (glAccountForSavings.equals(CashAccountsForSavings.TRANSFERS_SUSPENSE)) { - accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), gLAccountData); - } else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_ON_SAVINGS)) { - accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), gLAccountData); - } else if (glAccountForSavings.equals(CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL)) { - accountMappingDetails.put(SavingProductAccountingDataParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), gLAccountData); - } else if (glAccountForSavings.equals(CashAccountsForSavings.LOSSES_WRITTEN_OFF)) { - accountMappingDetails.put(SavingProductAccountingDataParams.LOSSES_WRITTEN_OFF.getValue(), gLAccountData); - } else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_INTEREST)) { - accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), gLAccountData); - } else if (glAccountForSavings.equals(CashAccountsForSavings.ESCHEAT_LIABILITY)) { - accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), gLAccountData); - } - } } + return accountMappingDetails; } @@ -412,4 +388,105 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product return fetchChargeToIncomeAccountMappings(PortfolioProductType.SHARES, productId, false); } + private Map setAccrualPeriodicSavingsProductToGLAccountMaps( + final List> listOfProductToGLAccountMaps) { + final Map accountMappingDetails = new LinkedHashMap<>(8); + + for (final Map productToGLAccountMap : listOfProductToGLAccountMaps) { + + final Integer financialAccountType = (Integer) productToGLAccountMap.get("financialAccountType"); + AccrualAccountsForSavings glAccountForSavings = AccrualAccountsForSavings.fromInt(financialAccountType); + + if (glAccountForSavings != null) { + final Long glAccountId = (Long) productToGLAccountMap.get("glAccountId"); + final String glAccountName = (String) productToGLAccountMap.get("glAccountName"); + final String glCode = (String) productToGLAccountMap.get("glCode"); + final GLAccountData glAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); + + // Assets + if (glAccountForSavings.equals(AccrualAccountsForSavings.SAVINGS_REFERENCE)) { + accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_REFERENCE.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)) { + accountMappingDetails.put(SavingProductAccountingDataParams.FEES_RECEIVABLE.getValue(), glAccountData); + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.PENALTIES_RECEIVABLE)) { + accountMappingDetails.put(SavingProductAccountingDataParams.PENALTIES_RECEIVABLE.getValue(), glAccountData); + // Liabilities + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.SAVINGS_CONTROL)) { + accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), glAccountData); + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.TRANSFERS_SUSPENSE)) { + accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), glAccountData); + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.INTEREST_PAYABLE)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_PAYABLE.getValue(), glAccountData); + // Income + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.INCOME_FROM_FEES)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), glAccountData); + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.INCOME_FROM_PENALTIES)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_PENALTIES.getValue(), glAccountData); + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.INCOME_FROM_INTEREST)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), glAccountData); + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.ESCHEAT_LIABILITY)) { + accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), glAccountData); + // Expense + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.INTEREST_ON_SAVINGS)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), glAccountData); + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.LOSSES_WRITTEN_OFF)) { + accountMappingDetails.put(SavingProductAccountingDataParams.LOSSES_WRITTEN_OFF.getValue(), glAccountData); + } + } else { + log.error("Accounting mapping null {}", financialAccountType); + } + } + + return accountMappingDetails; + } + + private Map setCashSavingsProductToGLAccountMaps(final List> listOfProductToGLAccountMaps) { + final Map accountMappingDetails = new LinkedHashMap<>(8); + + for (final Map productToGLAccountMap : listOfProductToGLAccountMaps) { + + final Integer financialAccountType = (Integer) productToGLAccountMap.get("financialAccountType"); + CashAccountsForSavings glAccountForSavings = CashAccountsForSavings.fromInt(financialAccountType); + + if (glAccountForSavings != null) { + final Long glAccountId = (Long) productToGLAccountMap.get("glAccountId"); + final String glAccountName = (String) productToGLAccountMap.get("glAccountName"); + final String glCode = (String) productToGLAccountMap.get("glCode"); + final GLAccountData glAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); + + // Assets + if (glAccountForSavings.equals(CashAccountsForSavings.SAVINGS_REFERENCE)) { + accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_REFERENCE.getValue(), glAccountData); + } else if (glAccountForSavings.equals(CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL)) { + accountMappingDetails.put(SavingProductAccountingDataParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), glAccountData); + // Liabilities + } else if (glAccountForSavings.equals(CashAccountsForSavings.SAVINGS_CONTROL)) { + accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), glAccountData); + } else if (glAccountForSavings.equals(CashAccountsForSavings.TRANSFERS_SUSPENSE)) { + accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), glAccountData); + // Income + } else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_FEES)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), glAccountData); + } else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_PENALTIES)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_PENALTIES.getValue(), glAccountData); + } else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_INTEREST)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), glAccountData); + } else if (glAccountForSavings.equals(CashAccountsForSavings.ESCHEAT_LIABILITY)) { + accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), glAccountData); + // Expense + } else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_ON_SAVINGS)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), glAccountData); + } else if (glAccountForSavings.equals(CashAccountsForSavings.LOSSES_WRITTEN_OFF)) { + accountMappingDetails.put(SavingProductAccountingDataParams.LOSSES_WRITTEN_OFF.getValue(), glAccountData); + } + } else { + log.error("Accounting mapping null {}", financialAccountType); + } + } + + return accountMappingDetails; + } + } 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 6cccdfe1f..4b3e75b51 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 @@ -21,6 +21,7 @@ package org.apache.fineract.accounting.producttoaccountmapping.service; import com.google.gson.JsonElement; import java.util.HashMap; import java.util.Map; +import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.SavingProductAccountingParams; import org.apache.fineract.accounting.common.AccountingRuleType; @@ -167,6 +168,25 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa changes.put(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), writeOffId); break; case ACCRUAL_PERIODIC: + final Long feeReceivableId = this.fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), element); + final Long penaltyReceivableId = this.fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), element); + final Long interestPayableId = this.fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), element); + + changes.put(SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), savingsControlId); + changes.put(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), savingsReferenceId); + changes.put(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), interestOnSavingsId); + changes.put(SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), incomeFromFeesId); + changes.put(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), incomeFromPenaltiesId); + changes.put(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), transfersInSuspenseAccountId); + changes.put(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), overdraftControlId); + changes.put(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), incomeFromInterest); + changes.put(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), writeOffId); + changes.put(SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), feeReceivableId); + changes.put(SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), penaltyReceivableId); + changes.put(SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), interestPayableId); break; case ACCRUAL_UPFRONT: break; @@ -229,6 +249,61 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa savingsProductId, CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes); break; case ACCRUAL_PERIODIC: + // asset + mergeSavingsToAssetAccountMappingChanges(element, SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), + savingsProductId, AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), + AccrualAccountsForSavings.SAVINGS_REFERENCE.toString(), changes); + + mergeSavingsToAssetAccountMappingChanges(element, SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), + savingsProductId, AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.toString(), changes); + + mergeSavingsToAssetAccountMappingChanges(element, SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), + 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); + + // income + mergeSavingsToIncomeAccountMappingChanges(element, SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), + savingsProductId, AccrualAccountsForSavings.INCOME_FROM_FEES.getValue(), + AccrualAccountsForSavings.INCOME_FROM_FEES.toString(), changes); + + mergeSavingsToIncomeAccountMappingChanges(element, SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), + savingsProductId, AccrualAccountsForSavings.INCOME_FROM_PENALTIES.getValue(), + AccrualAccountsForSavings.INCOME_FROM_PENALTIES.toString(), changes); + + mergeSavingsToIncomeAccountMappingChanges(element, SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), + savingsProductId, AccrualAccountsForSavings.INCOME_FROM_INTEREST.getValue(), + AccrualAccountsForSavings.INCOME_FROM_INTEREST.toString(), changes); + + // expenses + mergeSavingsToExpenseAccountMappingChanges(element, SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), + savingsProductId, AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), + AccrualAccountsForSavings.INTEREST_ON_SAVINGS.toString(), changes); + + mergeSavingsToExpenseAccountMappingChanges(element, SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), + savingsProductId, AccrualAccountsForSavings.LOSSES_WRITTEN_OFF.getValue(), + AccrualAccountsForSavings.LOSSES_WRITTEN_OFF.toString(), changes); + + // liability + mergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), + savingsProductId, AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + AccrualAccountsForSavings.SAVINGS_CONTROL.toString(), changes); + + mergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), + savingsProductId, AccrualAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), + AccrualAccountsForSavings.TRANSFERS_SUSPENSE.toString(), changes); + + mergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), + savingsProductId, AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), + AccrualAccountsForSavings.INTEREST_PAYABLE.toString(), changes); + + createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), + savingsProductId, AccrualAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes); + break; case ACCRUAL_UPFRONT: break; 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 257dc1f94..669ad59dc 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 @@ -277,6 +277,54 @@ public final class AccountingConstants { } } + /*** + * Accounting placeholders for periodic accrual based accounting for savings products + ***/ + public enum AccrualAccountsForSavings { + + SAVINGS_REFERENCE(1), // + SAVINGS_CONTROL(2), // + INTEREST_ON_SAVINGS(3), // + INCOME_FROM_FEES(4), // + INCOME_FROM_PENALTIES(5), // + TRANSFERS_SUSPENSE(10), // + OVERDRAFT_PORTFOLIO_CONTROL(11), // + INCOME_FROM_INTEREST(12), // + LOSSES_WRITTEN_OFF(13), // + ESCHEAT_LIABILITY(14), // + FEES_RECEIVABLE(15), // + PENALTIES_RECEIVABLE(16), // + INTEREST_PAYABLE(17); + + private final Integer value; + + AccrualAccountsForSavings(final Integer value) { + this.value = value; + } + + @Override + public String toString() { + return name().toString().replaceAll("_", " "); + } + + public Integer getValue() { + return this.value; + } + + private static final Map intToEnumMap = new HashMap<>(); + + static { + for (final AccrualAccountsForSavings type : AccrualAccountsForSavings.values()) { + intToEnumMap.put(type.value, type); + } + } + + public static AccrualAccountsForSavings fromInt(final int i) { + final AccrualAccountsForSavings type = intToEnumMap.get(Integer.valueOf(i)); + return type; + } + } + /*** * Enum of all accounting related input parameter names used while creating/updating a savings product ***/ @@ -298,7 +346,10 @@ public final class AccountingConstants { OVERDRAFT_PORTFOLIO_CONTROL("overdraftPortfolioControlId"), // INCOME_FROM_INTEREST("incomeFromInterestId"), // LOSSES_WRITTEN_OFF("writeOffAccountId"), // - ESCHEAT_LIABILITY("escheatLiabilityId"); // + ESCHEAT_LIABILITY("escheatLiabilityId"), // + PENALTIES_RECEIVABLE("penaltiesReceivableAccountId"), // + FEES_RECEIVABLE("feesReceivableAccountId"), // + INTEREST_PAYABLE("interestPayableAccountId"); private final String value; @@ -332,7 +383,10 @@ public final class AccountingConstants { OVERDRAFT_PORTFOLIO_CONTROL("overdraftPortfolioControl"), // INCOME_FROM_INTEREST("incomeFromInterest"), // LOSSES_WRITTEN_OFF("writeOffAccount"), // - ESCHEAT_LIABILITY("escheatLiabilityAccount"); // + ESCHEAT_LIABILITY("escheatLiabilityAccount"), // + FEES_RECEIVABLE("feeReceivableAccount"), // + PENALTIES_RECEIVABLE("penaltyReceivableAccount"), // + INTEREST_PAYABLE("interestPayableAccount"); // private final String value; 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 517003b7f..166560395 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 @@ -218,8 +218,10 @@ public final class DepositsApiConstants { 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.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), chartsParamName, - SavingsApiConstants.withHoldTaxParamName, SavingsApiConstants.taxGroupIdParamName)); + SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), + SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), + SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), chartsParamName, SavingsApiConstants.withHoldTaxParamName, + SavingsApiConstants.taxGroupIdParamName)); private static final Set PRECLOSURE_REQUEST_DATA_PARAMETERS = new HashSet<>( Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnTypeIdParamName)); diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsAccountTransactionType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsAccountTransactionType.java index 639806c23..af1034abc 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsAccountTransactionType.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsAccountTransactionType.java @@ -40,6 +40,7 @@ public enum SavingsAccountTransactionType { WAIVE_CHARGES(6, "savingsAccountTransactionType.waiveCharge"), // PAY_CHARGE(7, "savingsAccountTransactionType.payCharge", TransactionEntryType.DEBIT), // DIVIDEND_PAYOUT(8, "savingsAccountTransactionType.dividendPayout", TransactionEntryType.CREDIT), // + ACCRUAL(10, "savingsAccountTransactionType.accrual"), // INITIATE_TRANSFER(12, "savingsAccountTransactionType.initiateTransfer"), // APPROVE_TRANSFER(13, "savingsAccountTransactionType.approveTransfer"), // WITHDRAW_TRANSFER(14, "savingsAccountTransactionType.withdrawTransfer"), // @@ -181,6 +182,10 @@ public enum SavingsAccountTransactionType { return this == AMOUNT_RELEASE; } + public boolean isAccrual() { + return this == ACCRUAL; + } + public boolean isCredit() { // AMOUNT_RELEASE is not credit, because the account balance is not changed return isCreditEntryType() && !isAmountRelease(); diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionEnumData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionEnumData.java index 60401d5b2..6d694690b 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionEnumData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionEnumData.java @@ -49,6 +49,7 @@ public class SavingsAccountTransactionEnumData implements Serializable { private final boolean escheat; private final boolean amountHold; private final boolean amountRelease; + private final boolean accrual; public SavingsAccountTransactionEnumData(final Long id, final String code, final String value) { this.id = id; @@ -57,6 +58,7 @@ public class SavingsAccountTransactionEnumData implements Serializable { SavingsAccountTransactionType transactionType = id == null ? null : SavingsAccountTransactionType.fromInt(id.intValue()); this.deposit = transactionType == SavingsAccountTransactionType.DEPOSIT; this.dividendPayout = transactionType == SavingsAccountTransactionType.DIVIDEND_PAYOUT; + this.accrual = transactionType == SavingsAccountTransactionType.ACCRUAL; this.withdrawal = transactionType == SavingsAccountTransactionType.WITHDRAWAL; this.interestPosting = transactionType == SavingsAccountTransactionType.INTEREST_POSTING; this.feeDeduction = transactionType == SavingsAccountTransactionType.ANNUAL_FEE diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java index 9d14a871f..21c594362 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java @@ -141,6 +141,10 @@ public final class SavingsEnumerations { optionData = new SavingsAccountTransactionEnumData(SavingsAccountTransactionType.WITHDRAWAL.getValue().longValue(), SavingsAccountTransactionType.WITHDRAWAL.getCode(), "Withdrawal"); break; + case ACCRUAL: + optionData = new SavingsAccountTransactionEnumData(SavingsAccountTransactionType.ACCRUAL.getValue().longValue(), + SavingsAccountTransactionType.ACCRUAL.getCode(), "Accrual"); + break; case INTEREST_POSTING: optionData = new SavingsAccountTransactionEnumData(SavingsAccountTransactionType.INTEREST_POSTING.getValue().longValue(), SavingsAccountTransactionType.INTEREST_POSTING.getCode(), "Interest posting"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavingsFactory.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavingsFactory.java index e5c9fb4b5..5bccce4e2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavingsFactory.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavingsFactory.java @@ -40,6 +40,9 @@ public class AccountingProcessorForSavingsFactory { if (savingsDTO.isCashBasedAccountingEnabled()) { accountingProcessorForSavings = this.applicationContext.getBean("cashBasedAccountingProcessorForSavings", AccountingProcessorForSavings.class); + } else if (savingsDTO.isAccrualBasedAccountingEnabled()) { + accountingProcessorForSavings = this.applicationContext.getBean("accrualBasedAccountingProcessorForSavings", + AccountingProcessorForSavings.class); } return accountingProcessorForSavings; 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 6c637e9a4..804184c3e 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 @@ -30,6 +30,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.closure.domain.GLClosure; import org.apache.fineract.accounting.closure.domain.GLClosureRepository; import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan; +import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares; @@ -573,6 +574,28 @@ public class AccountingProcessorHelper { paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); } + public void createAccrualBasedJournalEntriesAndReversalsForSavingsTax(final Office office, final String currencyCode, + final AccrualAccountsForSavings accountTypeToBeDebited, final AccrualAccountsForSavings accountTypeToBeCredited, + final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, + final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal, final List taxDetails) { + + for (TaxPaymentDTO taxPaymentDTO : taxDetails) { + if (taxPaymentDTO.getAmount() != null) { + if (taxPaymentDTO.getCreditAccountId() == null) { + createAccrualBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, accountTypeToBeCredited.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, taxPaymentDTO.getAmount(), + isReversal); + } else { + createAccrualBasedBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, + taxPaymentDTO.getCreditAccountId(), savingsId, transactionId, transactionDate, taxPaymentDTO.getAmount(), + isReversal); + } + } + } + createAccrualBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode, accountTypeToBeDebited.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + public void createCashBasedDebitJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, final Integer accountTypeToBeDebited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { @@ -623,6 +646,56 @@ public class AccountingProcessorHelper { } } + public void createAccrualBasedDebitJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Integer accountTypeToBeDebited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { + // reverse debits and credits for reversals + if (isReversal) { + createCreditJournalEntriesForSavings(office, currencyCode, accountTypeToBeDebited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } else { + createDebitJournalEntriesForSavings(office, currencyCode, accountTypeToBeDebited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } + } + + public void createAccrualBasedCreditJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Integer accountTypeToBeCredited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { + // reverse debits and credits for reversals + if (isReversal) { + createDebitJournalEntriesForSavings(office, currencyCode, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } else { + createCreditJournalEntriesForSavings(office, currencyCode, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } + } + + public void createAccrualBasedDebitJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Long debitAccountId, final Long savingsId, final String transactionId, final LocalDate transactionDate, + final BigDecimal amount, final Boolean isReversal) { + // reverse debits and credits for reversals + final GLAccount debitAccount = getGLAccountById(debitAccountId); + if (isReversal) { + createCreditJournalEntryForSavings(office, currencyCode, debitAccount, savingsId, transactionId, transactionDate, amount); + } else { + createDebitJournalEntryForSavings(office, currencyCode, debitAccount, savingsId, transactionId, transactionDate, amount); + } + } + + public void createAccrualBasedBasedCreditJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Long creditAccountId, final Long savingsId, final String transactionId, final LocalDate transactionDate, + final BigDecimal amount, final Boolean isReversal) { + // reverse debits and credits for reversals + final GLAccount creditAccount = getGLAccountById(creditAccountId); + if (isReversal) { + createDebitJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); + } else { + createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); + } + } + private void createDebitJournalEntriesForSavings(final Office office, final String currencyCode, final int accountTypeToDebitId, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { @@ -768,6 +841,41 @@ public class AccountingProcessorHelper { } } + public void createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(final Office office, final String currencyCode, + final AccrualAccountsForSavings accountTypeToBeDebited, AccrualAccountsForSavings accountTypeToBeCredited, + final Long savingsProductId, final Long paymentTypeId, final Long loanId, final String transactionId, + final LocalDate transactionDate, final BigDecimal totalAmount, final Boolean isReversal, + final List chargePaymentDTOs) { + // TODO Vishwas: Remove this validation, as and when appropriate Junit + // tests are written for accounting + /** + * Accounting module currently supports a single charge per transaction, throw an error if this is not the case + * here so any developers changing the expected portfolio behavior would also take care of modifying the + * accounting code appropriately + **/ + if (chargePaymentDTOs.size() != 1) { + throw new PlatformDataIntegrityException("Recent Portfolio changes w.r.t Charges for Savings have Broken the accounting code", + "Recent Portfolio changes w.r.t Charges for Savings have Broken the accounting code"); + } + ChargePaymentDTO chargePaymentDTO = chargePaymentDTOs.get(0); + GLAccount chargeSpecificAccount = getLinkedGLAccountForSavingsCharges(savingsProductId, accountTypeToBeCredited.getValue(), + chargePaymentDTO.getChargeId()); + + final GLAccount savingsControlAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToBeDebited.getValue(), + paymentTypeId); + if (isReversal) { + createDebitJournalEntryForSavings(office, currencyCode, chargeSpecificAccount, loanId, transactionId, transactionDate, + totalAmount); + createCreditJournalEntryForSavings(office, currencyCode, savingsControlAccount, loanId, transactionId, transactionDate, + totalAmount); + } else { + createDebitJournalEntryForSavings(office, currencyCode, savingsControlAccount, loanId, transactionId, transactionDate, + totalAmount); + createCreditJournalEntryForSavings(office, currencyCode, chargeSpecificAccount, loanId, transactionId, transactionDate, + totalAmount); + } + } + public LoanTransaction getLoanTransactionById(final Long loanTransactionId) { return this.loanTransactionRepository.getReferenceById(loanTransactionId); } 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 new file mode 100644 index 000000000..4aa1b935b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java @@ -0,0 +1,272 @@ +/** + * 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.journalentry.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.closure.domain.GLClosure; +import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForSavings; +import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; +import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; +import org.apache.fineract.accounting.journalentry.data.SavingsDTO; +import org.apache.fineract.accounting.journalentry.data.SavingsTransactionDTO; +import org.apache.fineract.organisation.office.domain.Office; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AccrualBasedAccountingProcessorForSavings implements AccountingProcessorForSavings { + + private final AccountingProcessorHelper helper; + + @Override + public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { + final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); + final Long savingsProductId = savingsDTO.getSavingsProductId(); + final Long savingsId = savingsDTO.getSavingsId(); + final String currencyCode = savingsDTO.getCurrencyCode(); + for (final SavingsTransactionDTO savingsTransactionDTO : savingsDTO.getNewSavingsTransactions()) { + final LocalDate transactionDate = savingsTransactionDTO.getTransactionDate(); + final String transactionId = savingsTransactionDTO.getTransactionId(); + final Office office = this.helper.getOfficeById(savingsTransactionDTO.getOfficeId()); + final Long paymentTypeId = savingsTransactionDTO.getPaymentTypeId(); + final boolean isReversal = savingsTransactionDTO.isReversed(); + final BigDecimal amount = savingsTransactionDTO.getAmount(); + final BigDecimal overdraftAmount = savingsTransactionDTO.getOverdraftAmount(); + final List feePayments = savingsTransactionDTO.getFeePayments(); + final List penaltyPayments = savingsTransactionDTO.getPenaltyPayments(); + + this.helper.checkForBranchClosures(latestGLClosure, transactionDate); + + if (savingsTransactionDTO.getTransactionType().isWithdrawal() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), + FinancialActivity.LIABILITY_TRANSFER.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, + transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); + } + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); + } + } + } + + else if (savingsTransactionDTO.getTransactionType().isDeposit() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.LIABILITY_TRANSFER.getValue(), + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.LIABILITY_TRANSFER.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); + } + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); + } + } + } + + /** Handle Deposits and reversals of deposits **/ + else if (savingsTransactionDTO.getTransactionType().isDeposit()) { + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.LIABILITY_TRANSFER.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + } + + /** Handle Deposits and reversals of Dividend pay outs **/ + else if (savingsTransactionDTO.getTransactionType().isDividendPayout()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.PAYABLE_DIVIDENDS.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + + /** Handle withdrawals and reversals of withdrawals **/ + else if (savingsTransactionDTO.getTransactionType().isWithdrawal()) { + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + } + + else if (savingsTransactionDTO.getTransactionType().isEscheat()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.ESCHEAT_LIABILITY.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + /** + * Handle Interest Applications and reversals of Interest Applications + **/ + else if (savingsTransactionDTO.getTransactionType().isInterestPosting() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + // Post journal entry if earned interest amount is greater than + // zero + if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + 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); + } + } + } + + else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { + // Post journal entry if earned interest amount is greater than + // zero + if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + } + + 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); + } + } + + else if (savingsTransactionDTO.getTransactionType().isWithholdTax()) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsTax(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.SAVINGS_REFERENCE, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, + savingsTransactionDTO.getTaxPayments()); + } + + /** Handle Fees Deductions and reversals of Fees Deductions **/ + else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + // Is the Charge a penalty? + if (penaltyPayments.size() > 0) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, + penaltyPayments); + if (isPositive) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal, penaltyPayments); + } + } else { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, + feePayments); + if (isPositive) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, + feePayments); + } + } + } + + else if (savingsTransactionDTO.getTransactionType().isFeeDeduction()) { + // Is the Charge a penalty? + if (penaltyPayments.size() > 0) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, penaltyPayments); + } else { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); + } + } + + /** Handle Transfers proposal **/ + else if (savingsTransactionDTO.getTransactionType().isInitiateTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + + /** Handle Transfer Withdrawal or Acceptance **/ + else if (savingsTransactionDTO.getTransactionType().isWithdrawTransfer() + || savingsTransactionDTO.getTransactionType().isApproveTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + + /** overdraft **/ + else if (savingsTransactionDTO.getTransactionType().isOverdraftInterest()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), AccrualAccountsForSavings.INCOME_FROM_INTEREST.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else if (savingsTransactionDTO.getTransactionType().isWrittenoff()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.LOSSES_WRITTEN_OFF.getValue(), + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + } else if (savingsTransactionDTO.getTransactionType().isOverdraftFee()) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_REFERENCE, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); + } + } + } +} 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 751bc651b..53496003a 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 @@ -27,6 +27,7 @@ import java.util.Locale; import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan; +import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares; @@ -211,6 +212,65 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc } } + 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()); + + if (!accountType.equals(DepositAccountType.RECURRING_DEPOSIT) && !accountType.equals(DepositAccountType.FIXED_DEPOSIT)) { + this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element, + SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingProductId, + CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue()); + } + + // income + this.savingsProductToGLAccountMappingHelper.saveSavingsToIncomeAccountMapping(element, + SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), savingProductId, + CashAccountsForSavings.INCOME_FROM_FEES.getValue()); + + this.savingsProductToGLAccountMappingHelper.saveSavingsToIncomeAccountMapping(element, + SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), savingProductId, + CashAccountsForSavings.INCOME_FROM_PENALTIES.getValue()); + + if (!accountType.equals(DepositAccountType.RECURRING_DEPOSIT) && !accountType.equals(DepositAccountType.FIXED_DEPOSIT)) { + this.savingsProductToGLAccountMappingHelper.saveSavingsToIncomeAccountMapping(element, + SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), savingProductId, + CashAccountsForSavings.INCOME_FROM_INTEREST.getValue()); + } + + // expenses + this.savingsProductToGLAccountMappingHelper.saveSavingsToExpenseAccountMapping(element, + SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), savingProductId, + CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue()); + + if (!accountType.equals(DepositAccountType.RECURRING_DEPOSIT) && !accountType.equals(DepositAccountType.FIXED_DEPOSIT)) { + this.savingsProductToGLAccountMappingHelper.saveSavingsToExpenseAccountMapping(element, + SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), savingProductId, + CashAccountsForSavings.LOSSES_WRITTEN_OFF.getValue()); + } + + // liability + this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element, + SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), savingProductId, + CashAccountsForSavings.SAVINGS_CONTROL.getValue()); + this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element, + SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), savingProductId, + CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue()); + + final Boolean isDormancyTrackingActive = this.fromApiJsonHelper.extractBooleanNamed(isDormancyTrackingActiveParamName, element); + if (null != isDormancyTrackingActive && isDormancyTrackingActive) { + this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element, + SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), savingProductId, + CashAccountsForSavings.ESCHEAT_LIABILITY.getValue()); + } + + // advanced accounting mappings + this.savingsProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, savingProductId, null); + this.savingsProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, savingProductId, null); + } + @Override @Transactional public void createSavingProductToGLAccountMapping(final Long savingProductId, final JsonCommand command, @@ -219,67 +279,28 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc final Integer accountingRuleTypeId = this.fromApiJsonHelper.extractIntegerNamed(accountingRuleParamName, element, Locale.getDefault()); final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountingRuleTypeId); - switch (accountingRuleType) { case NONE: break; case CASH_BASED: - // asset + saveSavingsBaseAccountMapping(savingProductId, accountType, command, element); + break; + + case ACCRUAL_PERIODIC: + saveSavingsBaseAccountMapping(savingProductId, accountType, command, element); + // assets this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element, - SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), savingProductId, - CashAccountsForSavings.SAVINGS_REFERENCE.getValue()); + SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), savingProductId, + AccrualAccountsForSavings.FEES_RECEIVABLE.getValue()); - if (!accountType.equals(DepositAccountType.RECURRING_DEPOSIT) && !accountType.equals(DepositAccountType.FIXED_DEPOSIT)) { - this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element, - SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingProductId, - CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue()); - } - - // income - this.savingsProductToGLAccountMappingHelper.saveSavingsToIncomeAccountMapping(element, - SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), savingProductId, - CashAccountsForSavings.INCOME_FROM_FEES.getValue()); - - this.savingsProductToGLAccountMappingHelper.saveSavingsToIncomeAccountMapping(element, - SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), savingProductId, - CashAccountsForSavings.INCOME_FROM_PENALTIES.getValue()); - - if (!accountType.equals(DepositAccountType.RECURRING_DEPOSIT) && !accountType.equals(DepositAccountType.FIXED_DEPOSIT)) { - this.savingsProductToGLAccountMappingHelper.saveSavingsToIncomeAccountMapping(element, - SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), savingProductId, - CashAccountsForSavings.INCOME_FROM_INTEREST.getValue()); - } - - // expenses - this.savingsProductToGLAccountMappingHelper.saveSavingsToExpenseAccountMapping(element, - SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), savingProductId, - CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue()); - - if (!accountType.equals(DepositAccountType.RECURRING_DEPOSIT) && !accountType.equals(DepositAccountType.FIXED_DEPOSIT)) { - this.savingsProductToGLAccountMappingHelper.saveSavingsToExpenseAccountMapping(element, - SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), savingProductId, - CashAccountsForSavings.LOSSES_WRITTEN_OFF.getValue()); - } + this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element, + SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), savingProductId, + AccrualAccountsForSavings.PENALTIES_RECEIVABLE.getValue()); // liability this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element, - SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), savingProductId, - CashAccountsForSavings.SAVINGS_CONTROL.getValue()); - this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element, - SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), savingProductId, - CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue()); - - final Boolean isDormancyTrackingActive = this.fromApiJsonHelper.extractBooleanNamed(isDormancyTrackingActiveParamName, - element); - if (null != isDormancyTrackingActive && isDormancyTrackingActive) { - this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element, - SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), savingProductId, - CashAccountsForSavings.ESCHEAT_LIABILITY.getValue()); - } - - // advanced accounting mappings - this.savingsProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, savingProductId, null); - this.savingsProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, savingProductId, null); + SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), savingProductId, + AccrualAccountsForSavings.INTEREST_PAYABLE.getValue()); break; default: break; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index b99e1bd31..946d1e565 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -359,6 +359,8 @@ public class LoanScheduleAssembler { CalendarInstance compoundingCalendarInstance = null; InterestRecalculationCompoundingMethod compoundingMethod = null; boolean allowCompoundingOnEod = false; + final Boolean isFloatingInterestRate = this.fromApiJsonHelper + .extractBooleanNamed(LoanApiConstants.isFloatingInterestRateParameterName, element); if (isInterestRecalculationEnabled) { LoanProductInterestRecalculationDetails loanProductInterestRecalculationDetails = loanProduct .getProductInterestRecalculationDetails(); @@ -409,8 +411,7 @@ public class LoanScheduleAssembler { if (loanProduct.isLinkedToFloatingInterestRate()) { final BigDecimal interestRateDiff = this.fromApiJsonHelper .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRateDifferentialParameterName, element); - final Boolean isFloatingInterestRate = this.fromApiJsonHelper - .extractBooleanNamed(LoanApiConstants.isFloatingInterestRateParameterName, element); + List baseLendingRatePeriods = null; try { baseLendingRatePeriods = this.floatingRatesReadPlatformService.retrieveBaseLendingRate().getRatePeriods(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 6a6abe9ec..c5ee18fbf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -33,10 +33,9 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; -import org.apache.fineract.accounting.common.AccountingRuleType; +import org.apache.fineract.accounting.common.AccountingValidations; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; @@ -65,7 +64,6 @@ import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyTy import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException; import org.springframework.stereotype.Component; -@Slf4j @RequiredArgsConstructor @Component public final class LoanProductDataValidator { @@ -613,7 +611,8 @@ public final class LoanProductDataValidator { final Integer accountingRuleType = this.fromApiJsonHelper.extractIntegerNamed(ACCOUNTING_RULE, element, Locale.getDefault()); baseDataValidator.reset().parameter(ACCOUNTING_RULE).value(accountingRuleType).notNull().inMinMaxRange(1, 4); - if (isCashBasedAccounting(accountingRuleType) || isAccrualBasedAccounting(accountingRuleType)) { + if (AccountingValidations.isCashBasedAccounting(accountingRuleType) + || AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) { final Long fundAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.FUND_SOURCE.getValue(), element); baseDataValidator.reset().parameter(LoanProductAccountingParams.FUND_SOURCE.getValue()).value(fundAccountId).notNull() @@ -709,7 +708,7 @@ public final class LoanProductDataValidator { } - if (isAccrualBasedAccounting(accountingRuleType)) { + if (AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) { final Long receivableInterestAccountId = this.fromApiJsonHelper .extractLongNamed(LoanProductAccountingParams.INTEREST_RECEIVABLE.getValue(), element); @@ -2144,22 +2143,6 @@ public final class LoanProductDataValidator { } } - private boolean isCashBasedAccounting(final Integer accountingRuleType) { - return AccountingRuleType.CASH_BASED.getValue().equals(accountingRuleType); - } - - private boolean isAccrualBasedAccounting(final Integer accountingRuleType) { - return isUpfrontAccrualAccounting(accountingRuleType) || isPeriodicAccounting(accountingRuleType); - } - - private boolean isUpfrontAccrualAccounting(final Integer accountingRuleType) { - return AccountingRuleType.ACCRUAL_UPFRONT.getValue().equals(accountingRuleType); - } - - private boolean isPeriodicAccounting(final Integer accountingRuleType) { - return AccountingRuleType.ACCRUAL_PERIODIC.getValue().equals(accountingRuleType); - } - private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResourceSwagger.java index 96de367d0..75981cf5a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResourceSwagger.java @@ -318,88 +318,31 @@ final class FixedDepositProductsApiResourceSwagger { public String description; } + static final class GetFixedDepositProductsGlAccount { + + private GetFixedDepositProductsGlAccount() {} + + @Schema(example = "12") + public Long id; + @Schema(example = "savings ref") + public String name; + @Schema(example = "20") + public Integer glCode; + } + static final class GetFixedDepositProductsProductIdAccountingMappings { private GetFixedDepositProductsProductIdAccountingMappings() {} - static final class GetFixedDepositProductsProductIdSavingsReferenceAccount { - - private GetFixedDepositProductsProductIdSavingsReferenceAccount() {} - - @Schema(example = "12") - public Long id; - @Schema(example = "savings ref") - public String name; - @Schema(example = "20") - public String glCode; - } - - static final class GetFixedDepositProductsProductIdIncomeFromFeeAccount { - - private GetFixedDepositProductsProductIdIncomeFromFeeAccount() {} - - @Schema(example = "16") - public Long id; - @Schema(example = "income from savings fee") - public String name; - @Schema(example = "24") - public String glCode; - } - - static final class GetFixedDepositProductsProductIdIncomeFromPenaltyAccount { - - private GetFixedDepositProductsProductIdIncomeFromPenaltyAccount() {} - - @Schema(example = "17") - public Long id; - @Schema(example = "income from sav penalties") - public String name; - @Schema(example = "25") - public String glCode; - } - - static final class GetFixedDepositProductsProductIdInterestOnSavingsAccount { - - private GetFixedDepositProductsProductIdInterestOnSavingsAccount() {} - - @Schema(example = "15") - public Long id; - @Schema(example = "interest on savings") - public String name; - @Schema(example = "23") - public String glCode; - } - - static final class GetFixedDepositProductsProductIdSavingsControlAccount { - - private GetFixedDepositProductsProductIdSavingsControlAccount() {} - - @Schema(example = "13") - public Long id; - @Schema(example = "savings ref tool kit") - public String name; - @Schema(example = "21") - public String glCode; - } - - static final class GetFixedDepositProductsProductIdTransfersInSuspenseAccount { - - private GetFixedDepositProductsProductIdTransfersInSuspenseAccount() {} - - @Schema(example = "14") - public Long id; - @Schema(example = "saving transfers") - public String name; - @Schema(example = "22") - public String glCode; - } - - public GetFixedDepositProductsProductIdSavingsReferenceAccount savingsReferenceAccount; - public GetFixedDepositProductsProductIdIncomeFromFeeAccount incomeFromFeeAccount; - public GetFixedDepositProductsProductIdIncomeFromPenaltyAccount incomeFromPenaltyAccount; - public GetFixedDepositProductsProductIdInterestOnSavingsAccount interestOnSavingsAccount; - public GetFixedDepositProductsProductIdSavingsControlAccount savingsControlAccount; - public GetFixedDepositProductsProductIdTransfersInSuspenseAccount transfersInSuspenseAccount; + public GetFixedDepositProductsGlAccount savingsReferenceAccount; + public GetFixedDepositProductsGlAccount feeReceivableAccount; + public GetFixedDepositProductsGlAccount penaltyReceivableAccount; + public GetFixedDepositProductsGlAccount incomeFromFeeAccount; + public GetFixedDepositProductsGlAccount incomeFromPenaltyAccount; + public GetFixedDepositProductsGlAccount interestOnSavingsAccount; + public GetFixedDepositProductsGlAccount savingsControlAccount; + public GetFixedDepositProductsGlAccount transfersInSuspenseAccount; + public GetFixedDepositProductsGlAccount interestPayableAccount; } static final class GetFixedDepositProductsProductIdFeeToIncomeAccountMappings { @@ -455,7 +398,7 @@ final class FixedDepositProductsApiResourceSwagger { } public GetFixedDepositProductsProductIdPenaltyToIncomeAccountMappingsCharge charge; - public GetFixedDepositProductsProductIdAccountingMappings.GetFixedDepositProductsProductIdIncomeFromPenaltyAccount incomeAccount; + public GetFixedDepositProductsGlAccount incomeAccount; } static final class GetFixedDepositProductsProductIdPreClosurePenalInterestOnType { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResourceSwagger.java index e07b32c5d..57f5dd056 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResourceSwagger.java @@ -343,88 +343,30 @@ final class RecurringDepositProductsApiResourceSwagger { public String description; } + static final class GetRecurringDepositProductsGlAccount { + + private GetRecurringDepositProductsGlAccount() {} + + @Schema(example = "12") + public Long id; + @Schema(example = "savings control") + public String name; + @Schema(example = "2000001") + public String glCode; + } + static final class GetRecurringDepositProductsProductIdAccountingMappings { private GetRecurringDepositProductsProductIdAccountingMappings() {} - static final class GetRecurringDepositProductsProductIdSavingsReferenceAccount { - - private GetRecurringDepositProductsProductIdSavingsReferenceAccount() {} - - @Schema(example = "12") - public Long id; - @Schema(example = "savings ref") - public String name; - @Schema(example = "20") - public String glCode; - } - - static final class GetRecurringDepositProductsProductIdIncomeFromFeeAccount { - - private GetRecurringDepositProductsProductIdIncomeFromFeeAccount() {} - - @Schema(example = "16") - public Long id; - @Schema(example = "income from savings fee") - public String name; - @Schema(example = "24") - public String glCode; - } - - static final class GetRecurringDepositProductsProductIdIncomeFromPenaltyAccount { - - private GetRecurringDepositProductsProductIdIncomeFromPenaltyAccount() {} - - @Schema(example = "17") - public Long id; - @Schema(example = "income from sav penalties") - public String name; - @Schema(example = "25") - public String glCode; - } - - static final class GetRecurringDepositProductsProductIdInterestOnSavingsAccount { - - private GetRecurringDepositProductsProductIdInterestOnSavingsAccount() {} - - @Schema(example = "15") - public Long id; - @Schema(example = "interest on savings") - public String name; - @Schema(example = "23") - public String glCode; - } - - static final class GetRecurringDepositProductsProductIdSavingsControlAccount { - - private GetRecurringDepositProductsProductIdSavingsControlAccount() {} - - @Schema(example = "13") - public Long id; - @Schema(example = "savings ref tool kit") - public String name; - @Schema(example = "21") - public String glCode; - } - - static final class GetRecurringDepositProductsProductIdTransfersInSuspenseAccount { - - private GetRecurringDepositProductsProductIdTransfersInSuspenseAccount() {} - - @Schema(example = "14") - public Long id; - @Schema(example = "saving transfers") - public String name; - @Schema(example = "22") - public String glCode; - } - - public GetRecurringDepositProductsProductIdSavingsReferenceAccount savingsReferenceAccount; - public GetRecurringDepositProductsProductIdIncomeFromFeeAccount incomeFromFeeAccount; - public GetRecurringDepositProductsProductIdIncomeFromPenaltyAccount incomeFromPenaltyAccount; - public GetRecurringDepositProductsProductIdInterestOnSavingsAccount interestOnSavingsAccount; - public GetRecurringDepositProductsProductIdSavingsControlAccount savingsControlAccount; - public GetRecurringDepositProductsProductIdTransfersInSuspenseAccount transfersInSuspenseAccount; + public GetRecurringDepositProductsGlAccount incomeFromFeeAccount; + public GetRecurringDepositProductsGlAccount incomeFromPenaltyAccount; + public GetRecurringDepositProductsGlAccount interestOnSavingsAccount; + public GetRecurringDepositProductsGlAccount savingsControlAccount; + public GetRecurringDepositProductsGlAccount transfersInSuspenseAccount; + public GetRecurringDepositProductsGlAccount feeReceivableAccount; + public GetRecurringDepositProductsGlAccount penaltyReceivableAccount; + public GetRecurringDepositProductsGlAccount interestPayableAccount; } static final class GetRecurringDepositProductsProductIdFeeToIncomeAccountMappings { @@ -480,7 +422,7 @@ final class RecurringDepositProductsApiResourceSwagger { } public GetRecurringDepositProductsProductIdPenaltyToIncomeAccountMappingsCharge charge; - public GetRecurringDepositProductsProductIdAccountingMappings.GetRecurringDepositProductsProductIdIncomeFromPenaltyAccount incomeAccount; + public GetRecurringDepositProductsGlAccount incomeAccount; } static final class GetRecurringDepositProductsProductIdPreClosurePenalInterestOnType { 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 b44afc2e7..f2fe2ef69 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 @@ -226,88 +226,34 @@ final class SavingsProductsApiResourceSwagger { private GetSavingsProductsProductIdResponse() {} + static final class GetSavingsProductsGlAccount { + + private GetSavingsProductsGlAccount() {} + + @Schema(example = "12") + public Integer id; + @Schema(example = "savings control") + public String name; + @Schema(example = "2000001") + public String glCode; + } + static final class GetSavingsProductsAccountingMappings { private GetSavingsProductsAccountingMappings() {} - static final class GetSavingsProductsSavingsReferenceAccount { - - private GetSavingsProductsSavingsReferenceAccount() {} - - @Schema(example = "12") - public Integer id; - @Schema(example = "savings ref") - public String name; - @Schema(example = "20") - public String glCode; - } - - static final class GetSavingsProductsIncomeFromFeeAccount { - - private GetSavingsProductsIncomeFromFeeAccount() {} - - @Schema(example = "16") - public Integer id; - @Schema(example = "income from savings fee") - public String name; - @Schema(example = "24") - public String glCode; - } - - static final class GetSavingsProductsIncomeFromPenaltyAccount { - - private GetSavingsProductsIncomeFromPenaltyAccount() {} - - @Schema(example = "17") - public Integer id; - @Schema(example = "income from sav penalties") - public String name; - @Schema(example = "25") - public String glCode; - } - - static final class GetSavingsProductsInterestOnSavingsAccount { - - private GetSavingsProductsInterestOnSavingsAccount() {} - - @Schema(example = "15") - public Integer id; - @Schema(example = "interest on savings") - public String name; - @Schema(example = "23") - public String glCode; - } - - static final class GetSavingsProductsSavingsControlAccount { - - private GetSavingsProductsSavingsControlAccount() {} - - @Schema(example = "13") - public Integer id; - @Schema(example = "savings ref tool kit") - public String name; - @Schema(example = "21") - public String glCode; - } - - static final class GetSavingsProductsTransfersInSuspenseAccount { - - private GetSavingsProductsTransfersInSuspenseAccount() {} - - @Schema(example = "14") - public Integer id; - @Schema(example = "saving transfers") - public String name; - @Schema(example = "22") - public String glCode; - } - - public GetSavingsProductsSavingsReferenceAccount savingsReferenceAccount; - public GetSavingsProductsIncomeFromFeeAccount incomeFromFeeAccount; - public GetSavingsProductsIncomeFromPenaltyAccount incomeFromPenaltyAccount; - public GetSavingsProductsInterestOnSavingsAccount interestOnSavingsAccount; - public GetSavingsProductsSavingsControlAccount savingsControlAccount; - public GetSavingsProductsTransfersInSuspenseAccount transfersInSuspenseAccount; + public GetSavingsProductsGlAccount savingsReferenceAccount; + public GetSavingsProductsGlAccount overdraftPortfolioControl; + public GetSavingsProductsGlAccount feeReceivableAccount; + public GetSavingsProductsGlAccount penaltyReceivableAccount; + public GetSavingsProductsGlAccount incomeFromFeeAccount; + public GetSavingsProductsGlAccount incomeFromPenaltyAccount; + public GetSavingsProductsGlAccount incomeFromInterest; + public GetSavingsProductsGlAccount interestOnSavingsAccount; + public GetSavingsProductsGlAccount writeOffAccount; + public GetSavingsProductsGlAccount savingsControlAccount; + public GetSavingsProductsGlAccount transfersInSuspenseAccount; + public GetSavingsProductsGlAccount interestPayableAccount; } static final class GetSavingsProductsPaymentChannelToFundSourceMappings { @@ -393,7 +339,7 @@ final class SavingsProductsApiResourceSwagger { } public GetSavingsProductsPenaltyToIncomeAccountMappingsCharge charge; - public GetSavingsProductsAccountingMappings.GetSavingsProductsIncomeFromPenaltyAccount incomeAccount; + public GetSavingsProductsGlAccount incomeAccount; } @Schema(example = "1") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java index 96380a82d..a30f97b63 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java @@ -71,35 +71,33 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.common.AccountingConstants.SavingProductAccountingParams; -import org.apache.fineract.accounting.common.AccountingRuleType; +import org.apache.fineract.accounting.common.AccountingValidations; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.interestratechart.data.InterestRateChartDataValidator; +import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.PreClosurePenalInterestOnType; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; 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.SavingsPeriodFrequencyType; import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public class DepositProductDataValidator { private final FromJsonHelper fromApiJsonHelper; private final InterestRateChartDataValidator chartDataValidator; - - @Autowired - public DepositProductDataValidator(FromJsonHelper fromApiJsonHelper, InterestRateChartDataValidator chartDataValidator) { - this.fromApiJsonHelper = fromApiJsonHelper; - this.chartDataValidator = chartDataValidator; - } + private final SavingsProductAccountingDataValidator savingsProductAccountingDataValidator; public void validateForFixedDepositCreate(final String json) { if (StringUtils.isBlank(json)) { @@ -115,7 +113,7 @@ public class DepositProductDataValidator { final JsonElement element = this.fromApiJsonHelper.parse(json); - validateDepositDetailForCreate(element, this.fromApiJsonHelper, baseDataValidator); + validateDepositDetailForCreate(element, this.fromApiJsonHelper, baseDataValidator, DepositAccountType.FIXED_DEPOSIT); validatePreClosureDetailForCreate(element, baseDataValidator); @@ -168,7 +166,7 @@ public class DepositProductDataValidator { final JsonElement element = this.fromApiJsonHelper.parse(json); - validateDepositDetailForCreate(element, this.fromApiJsonHelper, baseDataValidator); + validateDepositDetailForCreate(element, this.fromApiJsonHelper, baseDataValidator, DepositAccountType.RECURRING_DEPOSIT); validatePreClosureDetailForCreate(element, baseDataValidator); @@ -212,7 +210,7 @@ public class DepositProductDataValidator { } private void validateDepositDetailForCreate(final JsonElement element, final FromJsonHelper fromApiJsonHelper, - final DataValidatorBuilder baseDataValidator) { + final DataValidatorBuilder baseDataValidator, final DepositAccountType accountType) { final String name = fromApiJsonHelper.extractStringNamed(nameParamName, element); baseDataValidator.reset().parameter(nameParamName).value(name).notBlank().notExceedingLengthOf(100); @@ -258,13 +256,6 @@ public class DepositProductDataValidator { baseDataValidator.reset().parameter(interestCalculationDaysInYearTypeParamName).value(interestCalculationDaysInYearType).notNull() .isOneOfTheseValues(SavingsInterestCalculationDaysInYearType.integerValues()); - /* - * if (fromApiJsonHelper.parameterExists(minRequiredOpeningBalanceParamName , element)) { final BigDecimal - * minOpeningBalance = fromApiJsonHelper.extractBigDecimalWithLocaleNamed (minRequiredOpeningBalanceParamName, - * element); baseDataValidator.reset ().parameter(minRequiredOpeningBalanceParamName - * ).value(minOpeningBalance).zeroOrPositiveAmount(); } - */ - if (fromApiJsonHelper.parameterExists(lockinPeriodFrequencyParamName, element)) { final Integer lockinPeriodFrequency = fromApiJsonHelper.extractIntegerWithLocaleNamed(lockinPeriodFrequencyParamName, element); @@ -302,40 +293,16 @@ public class DepositProductDataValidator { final Integer accountingRuleType = fromApiJsonHelper.extractIntegerNamed("accountingRule", element, Locale.getDefault()); baseDataValidator.reset().parameter("accountingRule").value(accountingRuleType).notNull().inMinMaxRange(1, 3); - if (isCashBasedAccounting(accountingRuleType)) { + Boolean isDormancyActive = this.fromApiJsonHelper.extractBooleanNamed(SavingsApiConstants.isDormancyTrackingActiveParamName, + element); + if (isDormancyActive == null) { + isDormancyActive = false; + } - final Long savingsControlAccountId = fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.SAVINGS_CONTROL.getValue()).value(savingsControlAccountId) - .notNull().integerGreaterThanZero(); - - final Long savingsReferenceAccountId = fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue()).value(savingsReferenceAccountId) - .notNull().integerGreaterThanZero(); - - final Long transfersInSuspenseAccountId = fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue()) - .value(transfersInSuspenseAccountId).notNull().integerGreaterThanZero(); - - final Long interestOnSavingsAccountId = fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue()) - .value(interestOnSavingsAccountId).notNull().integerGreaterThanZero(); - - final Long incomeFromFeeId = fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), - element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_FEES.getValue()).value(incomeFromFeeId).notNull() - .integerGreaterThanZero(); - - final Long incomeFromPenaltyId = fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue()).value(incomeFromPenaltyId) - .notNull().integerGreaterThanZero(); - - validatePaymentChannelFundSourceMappings(fromApiJsonHelper, baseDataValidator, element); - validateChargeToIncomeAccountMappings(fromApiJsonHelper, baseDataValidator, element); + if (AccountingValidations.isCashBasedAccounting(accountingRuleType) + || AccountingValidations.isAccrualPeriodicBasedAccounting(accountingRuleType)) { + savingsProductAccountingDataValidator.evaluateProductAccountingData(accountingRuleType, isDormancyActive, element, + baseDataValidator, accountType); } validateTaxWithHoldingParams(baseDataValidator, element, true); @@ -542,8 +509,8 @@ public class DepositProductDataValidator { baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue()).value(incomeFromPenaltyId) .ignoreIfNull().integerGreaterThanZero(); - validatePaymentChannelFundSourceMappings(fromApiJsonHelper, baseDataValidator, element); - validateChargeToIncomeAccountMappings(fromApiJsonHelper, baseDataValidator, element); + savingsProductAccountingDataValidator.validatePaymentChannelFundSourceMappings(baseDataValidator, element); + savingsProductAccountingDataValidator.validateChargeToIncomeAccountMappings(baseDataValidator, element); validateTaxWithHoldingParams(baseDataValidator, element, false); } @@ -611,76 +578,6 @@ public class DepositProductDataValidator { } } - private boolean isCashBasedAccounting(final Integer accountingRuleType) { - return AccountingRuleType.CASH_BASED.getValue().equals(accountingRuleType); - } - - /** - * Validation for advanced accounting options - */ - private void validatePaymentChannelFundSourceMappings(final FromJsonHelper fromApiJsonHelper, - final DataValidatorBuilder baseDataValidator, final JsonElement element) { - if (fromApiJsonHelper.parameterExists(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element)) { - final JsonArray paymentChannelMappingArray = fromApiJsonHelper - .extractJsonArrayNamed(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element); - if (paymentChannelMappingArray != null && paymentChannelMappingArray.size() > 0) { - int i = 0; - do { - final JsonObject jsonObject = paymentChannelMappingArray.get(i).getAsJsonObject(); - final Long paymentTypeId = jsonObject.get(SavingProductAccountingParams.PAYMENT_TYPE.getValue()).getAsLong(); - final Long paymentSpecificFundAccountId = jsonObject.get(SavingProductAccountingParams.FUND_SOURCE.getValue()) - .getAsLong(); - baseDataValidator.reset() - .parameter(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue() + "[" + i + "]." - + SavingProductAccountingParams.PAYMENT_TYPE.toString()) - .value(paymentTypeId).notNull().integerGreaterThanZero(); - baseDataValidator.reset() - .parameter(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue() + "[" + i + "]." - + SavingProductAccountingParams.FUND_SOURCE.getValue()) - .value(paymentSpecificFundAccountId).notNull().integerGreaterThanZero(); - i++; - } while (i < paymentChannelMappingArray.size()); - } - } - } - - private void validateChargeToIncomeAccountMappings(final FromJsonHelper fromApiJsonHelper, final DataValidatorBuilder baseDataValidator, - final JsonElement element) { - // validate for both fee and penalty charges - validateChargeToIncomeAccountMappings(fromApiJsonHelper, baseDataValidator, element, true); - validateChargeToIncomeAccountMappings(fromApiJsonHelper, baseDataValidator, element, true); - } - - private void validateChargeToIncomeAccountMappings(final FromJsonHelper fromApiJsonHelper, final DataValidatorBuilder baseDataValidator, - final JsonElement element, final boolean isPenalty) { - String parameterName; - if (isPenalty) { - parameterName = SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(); - } else { - parameterName = SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(); - } - - if (fromApiJsonHelper.parameterExists(parameterName, element)) { - final JsonArray chargeToIncomeAccountMappingArray = fromApiJsonHelper.extractJsonArrayNamed(parameterName, element); - if (chargeToIncomeAccountMappingArray != null && chargeToIncomeAccountMappingArray.size() > 0) { - int i = 0; - do { - final JsonObject jsonObject = chargeToIncomeAccountMappingArray.get(i).getAsJsonObject(); - final Long chargeId = fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.CHARGE_ID.getValue(), - jsonObject); - final Long incomeAccountId = fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), jsonObject); - baseDataValidator.reset().parameter(parameterName + "[" + i + "]." + SavingProductAccountingParams.CHARGE_ID.getValue()) - .value(chargeId).notNull().integerGreaterThanZero(); - baseDataValidator.reset() - .parameter(parameterName + "[" + i + "]." + SavingProductAccountingParams.INCOME_ACCOUNT_ID.getValue()) - .value(incomeAccountId).notNull().integerGreaterThanZero(); - i++; - } while (i < chargeToIncomeAccountMappingArray.size()); - } - } - } - public void validateRecurringDetailForCreate(JsonElement element, DataValidatorBuilder baseDataValidator) { final Boolean isMandatoryDeposit = this.fromApiJsonHelper.extractBooleanNamed(isMandatoryDepositParamName, element); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDataValidator.java index fd950926f..a53e5eceb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDataValidator.java @@ -43,6 +43,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; @@ -55,15 +56,14 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.savings.domain.SavingsAccountSubStatusEnum; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; import org.apache.fineract.portfolio.savings.exception.TransactionBeforePivotDateNotAllowed; import org.apache.fineract.useradministration.domain.AppUser; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public class SavingsAccountTransactionDataValidator { private final FromJsonHelper fromApiJsonHelper; @@ -71,15 +71,6 @@ public class SavingsAccountTransactionDataValidator { Arrays.asList(transactionDateParamName, SavingsApiConstants.dateFormatParamName, SavingsApiConstants.localeParamName, transactionAmountParamName, lienAllowedParamName, SavingsApiConstants.reasonForBlockParamName)); private final ConfigurationDomainService configurationDomainService; - private final SavingsAccountAssembler savingAccountAssembler; - - @Autowired - public SavingsAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper, - final ConfigurationDomainService configurationDomainService, final SavingsAccountAssembler savingAccountAssembler) { - this.fromApiJsonHelper = fromApiJsonHelper; - this.configurationDomainService = configurationDomainService; - this.savingAccountAssembler = savingAccountAssembler; - } public void validateTransactionWithPivotDate(final LocalDate transactionDate, final SavingsAccount savingsAccount) { final boolean backdatedTxnsAllowedTill = this.configurationDomainService.retrievePivotDateConfig(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java index 3389a0153..822a08cab 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java @@ -99,7 +99,6 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.util.CollectionUtils; @RequiredArgsConstructor - public class DepositAccountReadPlatformServiceImpl implements DepositAccountReadPlatformService { private static final FixedDepositAccountMapper FIXED_DEPOSIT_ACCOUNT_MAPPER = new FixedDepositAccountMapper(); 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 d094213c5..4a983e313 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 @@ -611,7 +611,6 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final BigDecimal outstandingChargeAmount = null; final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "runningBalance"); final boolean reversed = rs.getBoolean("reversed"); - final boolean isManualTransaction = rs.getBoolean("manualTransaction"); final Long officeId = rs.getLong("officeId"); final BigDecimal cumulativeBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "cumulativeBalance"); @@ -684,59 +683,6 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead } return savingsAccountDataList; - // final String productName = rs.getString("productName"); - - // final String fieldOfficerName = rs.getString("fieldOfficerName"); - - // final String submittedByUsername = rs.getString("submittedByUsername"); - // final String submittedByFirstname = rs.getString("submittedByFirstname"); - // final String submittedByLastname = rs.getString("submittedByLastname"); - // - - // final String rejectedByUsername = rs.getString("rejectedByUsername"); - // final String rejectedByFirstname = rs.getString("rejectedByFirstname"); - // final String rejectedByLastname = rs.getString("rejectedByLastname"); - // - - // final String withdrawnByUsername = rs.getString("withdrawnByUsername"); - // final String withdrawnByFirstname = rs.getString("withdrawnByFirstname"); - // final String withdrawnByLastname = rs.getString("withdrawnByLastname"); - - // final String approvedByUsername = rs.getString("approvedByUsername"); - // final String approvedByFirstname = rs.getString("approvedByFirstname"); - // final String approvedByLastname = rs.getString("approvedByLastname"); - - // final String activatedByUsername = rs.getString("activatedByUsername"); - // final String activatedByFirstname = rs.getString("activatedByFirstname"); - // final String activatedByLastname = rs.getString("activatedByLastname"); - - // final String closedByUsername = rs.getString("closedByUsername"); - // final String closedByFirstname = rs.getString("closedByFirstname"); - // final String closedByLastname = rs.getString("closedByLastname"); - - // final String currencyName = rs.getString("currencyName"); - // final String currencyNameCode = rs.getString("currencyNameCode"); - // final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); - - /* - * final BigDecimal withdrawalFeeAmount = rs.getBigDecimal("withdrawalFeeAmount"); - * - * EnumOptionData withdrawalFeeType = null; final Integer withdrawalFeeTypeValue = - * JdbcSupport.getInteger(rs, "withdrawalFeeTypeEnum"); if (withdrawalFeeTypeValue != null) { - * withdrawalFeeType = SavingsEnumerations.withdrawalFeeType(withdrawalFeeTypeValue); } - */ - - /* - * final BigDecimal annualFeeAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "annualFeeAmount"); - * - * MonthDay annualFeeOnMonthDay = null; final Integer annualFeeOnMonth = JdbcSupport.getInteger(rs, - * "annualFeeOnMonth"); final Integer annualFeeOnDay = JdbcSupport.getInteger(rs, "annualFeeOnDay"); if - * (annualFeeAmount != null && annualFeeOnDay != null) { annualFeeOnMonthDay = new - * MonthDay(annualFeeOnMonth, annualFeeOnDay); } - * - * final LocalDate annualFeeNextDueDate = JdbcSupport.getLocalDate(rs, "annualFeeNextDueDate"); - */ - } } @@ -792,20 +738,12 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append("sa.min_required_opening_balance as minRequiredOpeningBalance, "); sqlBuilder.append("sa.lockin_period_frequency as lockinPeriodFrequency,"); sqlBuilder.append("sa.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); - // sqlBuilder.append("sa.withdrawal_fee_amount as - // withdrawalFeeAmount,"); - // sqlBuilder.append("sa.withdrawal_fee_type_enum as - // withdrawalFeeTypeEnum, "); + sqlBuilder.append("sa.allow_overdraft as allowOverdraft, "); sqlBuilder.append("sa.overdraft_limit as overdraftLimit, "); sqlBuilder.append("sa.nominal_annual_interest_rate_overdraft as nominalAnnualInterestRateOverdraft, "); sqlBuilder.append("sa.min_overdraft_for_interest_calculation as minOverdraftForInterestCalculation, "); - // sqlBuilder.append("sa.annual_fee_amount as annualFeeAmount,"); - // sqlBuilder.append("sa.annual_fee_on_month as annualFeeOnMonth, - // "); - // sqlBuilder.append("sa.annual_fee_on_day as annualFeeOnDay, "); - // sqlBuilder.append("sa.annual_fee_next_due_date as - // annualFeeNextDueDate, "); + sqlBuilder.append("sa.total_deposits_derived as totalDeposits, "); sqlBuilder.append("sa.total_withdrawals_derived as totalWithdrawals, "); sqlBuilder.append("sa.total_withdrawal_fees_derived as totalWithdrawalFees, "); @@ -981,14 +919,6 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType); } - /* - * final BigDecimal withdrawalFeeAmount = rs.getBigDecimal("withdrawalFeeAmount"); - * - * EnumOptionData withdrawalFeeType = null; final Integer withdrawalFeeTypeValue = - * JdbcSupport.getInteger(rs, "withdrawalFeeTypeEnum"); if (withdrawalFeeTypeValue != null) { - * withdrawalFeeType = SavingsEnumerations.withdrawalFeeType(withdrawalFeeTypeValue); } - */ - final boolean withdrawalFeeForTransfers = rs.getBoolean("withdrawalFeeForTransfers"); final boolean allowOverdraft = rs.getBoolean("allowOverdraft"); @@ -1004,16 +934,6 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final BigDecimal maxAllowedLienLimit = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "maxAllowedLienLimit"); final boolean lienAllowed = rs.getBoolean("lienAllowed"); - /* - * final BigDecimal annualFeeAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "annualFeeAmount"); - * - * MonthDay annualFeeOnMonthDay = null; final Integer annualFeeOnMonth = JdbcSupport.getInteger(rs, - * "annualFeeOnMonth"); final Integer annualFeeOnDay = JdbcSupport.getInteger(rs, "annualFeeOnDay"); if - * (annualFeeAmount != null && annualFeeOnDay != null) { annualFeeOnMonthDay = new - * MonthDay(annualFeeOnMonth, annualFeeOnDay); } - * - * final LocalDate annualFeeNextDueDate = JdbcSupport.getLocalDate(rs, "annualFeeNextDueDate"); - */ final BigDecimal totalDeposits = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalDeposits"); final BigDecimal totalWithdrawals = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawals"); final BigDecimal totalWithdrawalFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawalFees"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsDropdownReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsDropdownReadPlatformServiceImpl.java index aa201de80..f4fb6238a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsDropdownReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsDropdownReadPlatformServiceImpl.java @@ -59,11 +59,7 @@ public class SavingsDropdownReadPlatformServiceImpl implements SavingsDropdownRe SavingsEnumerations.compoundingInterestPeriodType(SavingsCompoundingInterestPeriodType.MONTHLY), SavingsEnumerations.compoundingInterestPeriodType(SavingsCompoundingInterestPeriodType.QUATERLY), SavingsEnumerations.compoundingInterestPeriodType(SavingsCompoundingInterestPeriodType.BI_ANNUAL), - SavingsEnumerations.compoundingInterestPeriodType(SavingsCompoundingInterestPeriodType.ANNUAL) - // // - // SavingsEnumerations.compoundingInterestPeriodType(SavingsCompoundingInterestPeriodType.NO_COMPOUNDING_SIMPLE_INTEREST) - // // - ); + SavingsEnumerations.compoundingInterestPeriodType(SavingsCompoundingInterestPeriodType.ANNUAL)); return allowedOptions; } diff --git a/fineract-savings/dependencies.gradle b/fineract-savings/dependencies.gradle index bd2a15867..467fc305f 100644 --- a/fineract-savings/dependencies.gradle +++ b/fineract-savings/dependencies.gradle @@ -25,6 +25,7 @@ dependencies { // implementation dependencies are directly used (compiled against) in src/main (and src/test) // implementation(project(path: ':fineract-core')) + implementation(project(path: ':fineract-accounting')) implementation( 'org.springframework.boot:spring-boot-starter-web', diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductAccountingDataValidator.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductAccountingDataValidator.java new file mode 100644 index 000000000..1e527e758 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductAccountingDataValidator.java @@ -0,0 +1,185 @@ +/** + * 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 com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.common.AccountingConstants.SavingProductAccountingParams; +import org.apache.fineract.accounting.common.AccountingValidations; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SavingsProductAccountingDataValidator { + + private final FromJsonHelper fromApiJsonHelper; + + public void evaluateProductAccountingData(final Integer accountingRuleType, final boolean isDormancyActive, final JsonElement element, + DataValidatorBuilder baseDataValidator, final DepositAccountType accountType) { + // GL Accounts for Cash or Accrual Periodic + if (AccountingValidations.isCashBasedAccounting(accountingRuleType) + || AccountingValidations.isAccrualPeriodicBasedAccounting(accountingRuleType)) { + + final Long savingsControlAccountId = fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.SAVINGS_CONTROL.getValue()).value(savingsControlAccountId) + .notNull().integerGreaterThanZero(); + + final Long savingsReferenceAccountId = fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue()).value(savingsReferenceAccountId) + .notNull().integerGreaterThanZero(); + + final Long transfersInSuspenseAccountId = fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue()) + .value(transfersInSuspenseAccountId).notNull().integerGreaterThanZero(); + + final Long interestOnSavingsAccountId = fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue()) + .value(interestOnSavingsAccountId).notNull().integerGreaterThanZero(); + + final Long incomeFromFeeId = fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), + element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_FEES.getValue()).value(incomeFromFeeId).notNull() + .integerGreaterThanZero(); + + final Long incomeFromPenaltyId = fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue()).value(incomeFromPenaltyId) + .notNull().integerGreaterThanZero(); + + if (!accountType.equals(DepositAccountType.RECURRING_DEPOSIT) && !accountType.equals(DepositAccountType.FIXED_DEPOSIT)) { + final Long overdraftControlAccountId = fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue()) + .value(overdraftControlAccountId).notNull().integerGreaterThanZero(); + + final Long incomeFromInterest = fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue()).value(incomeFromInterest) + .notNull().integerGreaterThanZero(); + + final Long writtenoff = fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), + element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue()).value(writtenoff).notNull() + .integerGreaterThanZero(); + } + + if (isDormancyActive) { + final Long escheatLiabilityAccountId = fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue()) + .value(escheatLiabilityAccountId).notNull().integerGreaterThanZero(); + } + } + + // GL Accounts for Accrual Period only + if (AccountingValidations.isAccrualPeriodicBasedAccounting(accountingRuleType)) { + final Long feeReceivableAccountId = fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), + element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.FEES_RECEIVABLE.getValue()).value(feeReceivableAccountId) + .notNull().integerGreaterThanZero(); + + final Long penaltyReceivableAccountId = fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue()) + .value(penaltyReceivableAccountId).notNull().integerGreaterThanZero(); + + final Long interestPayableAccountId = fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), element); + baseDataValidator.reset().parameter(SavingProductAccountingParams.INTEREST_PAYABLE.getValue()).value(interestPayableAccountId) + .notNull().integerGreaterThanZero(); + } + + validatePaymentChannelFundSourceMappings(baseDataValidator, element); + validateChargeToIncomeAccountMappings(baseDataValidator, element); + } + + /** + * Validation for advanced accounting options + */ + public void validatePaymentChannelFundSourceMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + if (fromApiJsonHelper.parameterExists(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element)) { + final JsonArray paymentChannelMappingArray = fromApiJsonHelper + .extractJsonArrayNamed(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element); + if (paymentChannelMappingArray != null && paymentChannelMappingArray.size() > 0) { + int i = 0; + do { + final JsonObject jsonObject = paymentChannelMappingArray.get(i).getAsJsonObject(); + final Long paymentTypeId = jsonObject.get(SavingProductAccountingParams.PAYMENT_TYPE.getValue()).getAsLong(); + final Long paymentSpecificFundAccountId = jsonObject.get(SavingProductAccountingParams.FUND_SOURCE.getValue()) + .getAsLong(); + baseDataValidator.reset() + .parameter(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue() + "[" + i + "]." + + SavingProductAccountingParams.PAYMENT_TYPE.toString()) + .value(paymentTypeId).notNull().integerGreaterThanZero(); + baseDataValidator.reset() + .parameter(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue() + "[" + i + "]." + + SavingProductAccountingParams.FUND_SOURCE.getValue()) + .value(paymentSpecificFundAccountId).notNull().integerGreaterThanZero(); + i++; + } while (i < paymentChannelMappingArray.size()); + } + } + } + + public void validateChargeToIncomeAccountMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + // validate for both fee and penalty charges + validateChargeToIncomeAccountMappings(baseDataValidator, element, true); + validateChargeToIncomeAccountMappings(baseDataValidator, element, true); + } + + private void validateChargeToIncomeAccountMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element, + final boolean isPenalty) { + String parameterName; + if (isPenalty) { + parameterName = SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(); + } else { + parameterName = SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(); + } + + if (this.fromApiJsonHelper.parameterExists(parameterName, element)) { + final JsonArray chargeToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element); + if (chargeToIncomeAccountMappingArray != null && chargeToIncomeAccountMappingArray.size() > 0) { + int i = 0; + do { + final JsonObject jsonObject = chargeToIncomeAccountMappingArray.get(i).getAsJsonObject(); + final Long chargeId = this.fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.CHARGE_ID.getValue(), + jsonObject); + final Long incomeAccountId = this.fromApiJsonHelper + .extractLongNamed(SavingProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), jsonObject); + baseDataValidator.reset().parameter(parameterName + "[" + i + "]." + SavingProductAccountingParams.CHARGE_ID.getValue()) + .value(chargeId).notNull().integerGreaterThanZero(); + baseDataValidator.reset() + .parameter(parameterName + "[" + i + "]." + SavingProductAccountingParams.INCOME_ACCOUNT_ID.getValue()) + .value(incomeAccountId).notNull().integerGreaterThanZero(); + i++; + } while (i < chargeToIncomeAccountMappingArray.size()); + } + } + } + +} 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 368c1a87a..bac911472 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 @@ -50,9 +50,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroup import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawalFeeForTransfersParamName; -import com.google.gson.JsonArray; import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.math.BigDecimal; @@ -64,28 +62,31 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.common.AccountingConstants.SavingProductAccountingParams; -import org.apache.fineract.accounting.common.AccountingRuleType; +import org.apache.fineract.accounting.common.AccountingValidations; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; 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.domain.SavingsProduct; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public class SavingsProductDataValidator { private final FromJsonHelper fromApiJsonHelper; + 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, @@ -95,11 +96,12 @@ public class SavingsProductDataValidator { SavingsApiConstants.withdrawalFeeTypeParamName, withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, SavingsApiConstants.accountingRuleParamName, SavingsApiConstants.chargesParamName, SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), - SavingProductAccountingParams.INTEREST_ON_SAVINGS.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.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), + SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), isDormancyTrackingActiveParamName, daysToDormancyParamName, @@ -109,11 +111,6 @@ public class SavingsProductDataValidator { SavingsApiConstants.maxAllowedLienLimitParamName, SavingsApiConstants.lienAllowedParamName, minBalanceForInterestCalculationParamName, withHoldTaxParamName, taxGroupIdParamName)); - @Autowired - public SavingsProductDataValidator(final FromJsonHelper fromApiJsonHelper) { - this.fromApiJsonHelper = fromApiJsonHelper; - } - public void validateForCreate(final String json) { if (StringUtils.isBlank(json)) { @@ -207,28 +204,6 @@ public class SavingsProductDataValidator { } } - /* - * if (this.fromApiJsonHelper.parameterExists(withdrawalFeeAmountParamName, element)) { - * - * final BigDecimal withdrawalFeeAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed - * (withdrawalFeeAmountParamName, element); baseDataValidator.reset().parameter - * (withdrawalFeeAmountParamName).value (withdrawalFeeAmount).zeroOrPositiveAmount(); - * - * if (withdrawalFeeAmount != null) { final Integer withdrawalFeeType = - * this.fromApiJsonHelper.extractIntegerSansLocaleNamed( withdrawalFeeTypeParamName, element); - * baseDataValidator.reset().parameter (withdrawalFeeTypeParamName).value(withdrawalFeeType) - * .isOneOfTheseValues(SavingsWithdrawalFeesType.integerValues()); } } - * - * if (this.fromApiJsonHelper.parameterExists(withdrawalFeeTypeParamName, element)) { final Integer - * withdrawalFeeType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed (withdrawalFeeTypeParamName, - * element); baseDataValidator.reset().parameter - * (withdrawalFeeTypeParamName).value(withdrawalFeeType).ignoreIfNull() .isOneOfTheseValues(1, 2); - * - * if (withdrawalFeeType != null) { final BigDecimal withdrawalFeeAmount = - * this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed( withdrawalFeeAmountParamName, element); - * baseDataValidator.reset().parameter (withdrawalFeeAmountParamName).value(withdrawalFeeAmount).notNull() - * .zeroOrPositiveAmount(); } } - */ if (this.fromApiJsonHelper.parameterExists(withdrawalFeeForTransfersParamName, element)) { final Boolean isWithdrawalFeeApplicableForTransfers = this.fromApiJsonHelper .extractBooleanNamed(withdrawalFeeForTransfersParamName, element); @@ -256,9 +231,12 @@ public class SavingsProductDataValidator { } // dormancy - final Boolean isDormancyActive = this.fromApiJsonHelper.extractBooleanNamed(isDormancyTrackingActiveParamName, element); + Boolean isDormancyActive = this.fromApiJsonHelper.extractBooleanNamed(isDormancyTrackingActiveParamName, element); + if (isDormancyActive == null) { + isDormancyActive = false; + } - if (null != isDormancyActive && isDormancyActive) { + if (isDormancyActive) { final Long daysToInact = this.fromApiJsonHelper.extractLongNamed(daysToInactiveParamName, element); baseDataValidator.reset().parameter(daysToInactiveParamName).value(daysToInact).notNull().longGreaterThanZero(); @@ -278,62 +256,10 @@ public class SavingsProductDataValidator { final Integer accountingRuleType = this.fromApiJsonHelper.extractIntegerNamed("accountingRule", element, Locale.getDefault()); baseDataValidator.reset().parameter("accountingRule").value(accountingRuleType).notNull().inMinMaxRange(1, 3); - if (isCashBasedAccounting(accountingRuleType)) { - - final Long savingsControlAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.SAVINGS_CONTROL.getValue()).value(savingsControlAccountId) - .notNull().integerGreaterThanZero(); - - final Long savingsReferenceAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue()).value(savingsReferenceAccountId) - .notNull().integerGreaterThanZero(); - - final Long transfersInSuspenseAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue()) - .value(transfersInSuspenseAccountId).notNull().integerGreaterThanZero(); - - final Long interestOnSavingsAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue()) - .value(interestOnSavingsAccountId).notNull().integerGreaterThanZero(); - - final Long incomeFromFeeId = this.fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), - element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_FEES.getValue()).value(incomeFromFeeId).notNull() - .integerGreaterThanZero(); - - final Long incomeFromPenaltyId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue()).value(incomeFromPenaltyId) - .notNull().integerGreaterThanZero(); - - final Long overdraftControlAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue()) - .value(overdraftControlAccountId).notNull().integerGreaterThanZero(); - - final Long incomeFromInterest = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue()).value(incomeFromInterest) - .notNull().integerGreaterThanZero(); - - final Long writtenoff = this.fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), - element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue()).value(writtenoff).notNull() - .integerGreaterThanZero(); - - if (null != isDormancyActive && isDormancyActive) { - final Long escheatLiabilityAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue()) - .value(escheatLiabilityAccountId).notNull().integerGreaterThanZero(); - } - - validatePaymentChannelFundSourceMappings(baseDataValidator, element); - validateChargeToIncomeAccountMappings(baseDataValidator, element); + if (AccountingValidations.isCashBasedAccounting(accountingRuleType) + || AccountingValidations.isAccrualPeriodicBasedAccounting(accountingRuleType)) { + savingsProductAccountingDataValidator.evaluateProductAccountingData(accountingRuleType, isDormancyActive, element, + baseDataValidator, DepositAccountType.SAVINGS_DEPOSIT); } validateOverdraftParams(baseDataValidator, element); @@ -457,56 +383,21 @@ public class SavingsProductDataValidator { baseDataValidator.reset().parameter(feeOnMonthDayParamName).value(monthDayOfAnnualFee).ignoreIfNull(); } - final Long savingsControlAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.SAVINGS_CONTROL.getValue()).value(savingsControlAccountId) - .ignoreIfNull().integerGreaterThanZero(); - - final Long savingsReferenceAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue()).value(savingsReferenceAccountId) - .ignoreIfNull().integerGreaterThanZero(); - - final Long transfersInSuspenseAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue()).value(transfersInSuspenseAccountId) - .ignoreIfNull().integerGreaterThanZero(); - - final Long interestOnSavingsAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue()).value(interestOnSavingsAccountId) - .ignoreIfNull().integerGreaterThanZero(); - - final Long incomeFromFeeId = this.fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), - element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_FEES.getValue()).value(incomeFromFeeId).ignoreIfNull() - .integerGreaterThanZero(); - - final Long incomeFromPenaltyId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue()).value(incomeFromPenaltyId) - .ignoreIfNull().integerGreaterThanZero(); - - final Long overdraftAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue()).value(overdraftAccountId) - .ignoreIfNull().integerGreaterThanZero(); - - final Long incomeFromInterest = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue()).value(incomeFromInterest) - .ignoreIfNull().integerGreaterThanZero(); - - final Long writtenoff = this.fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), - element); - baseDataValidator.reset().parameter(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue()).value(writtenoff).ignoreIfNull() - .integerGreaterThanZero(); + // accounting related data validation + final Integer accountingRuleType = this.fromApiJsonHelper.extractIntegerNamed("accountingRule", element, Locale.getDefault()); + baseDataValidator.reset().parameter("accountingRule").value(accountingRuleType).notNull().inMinMaxRange(1, 3); // dormancy final Boolean isDormancyActive = this.fromApiJsonHelper.parameterExists(isDormancyTrackingActiveParamName, element) ? this.fromApiJsonHelper.extractBooleanNamed(isDormancyTrackingActiveParamName, element) : product.isDormancyTrackingActive(); + if (AccountingValidations.isCashBasedAccounting(accountingRuleType) + || AccountingValidations.isAccrualPeriodicBasedAccounting(accountingRuleType)) { + savingsProductAccountingDataValidator.evaluateProductAccountingData(accountingRuleType, isDormancyActive, element, + baseDataValidator, DepositAccountType.SAVINGS_DEPOSIT); + } + if (null != isDormancyActive && isDormancyActive) { final Long daysToInact = this.fromApiJsonHelper.parameterExists(daysToInactiveParamName, element) ? this.fromApiJsonHelper.extractLongNamed(daysToInactiveParamName, element) @@ -536,8 +427,6 @@ public class SavingsProductDataValidator { } } - validatePaymentChannelFundSourceMappings(baseDataValidator, element); - validateChargeToIncomeAccountMappings(baseDataValidator, element); validateOverdraftParams(baseDataValidator, element); if (this.fromApiJsonHelper.parameterExists(minBalanceForInterestCalculationParamName, element)) { @@ -558,74 +447,6 @@ public class SavingsProductDataValidator { } } - private boolean isCashBasedAccounting(final Integer accountingRuleType) { - return AccountingRuleType.CASH_BASED.getValue().equals(accountingRuleType); - } - - /** - * Validation for advanced accounting options - */ - private void validatePaymentChannelFundSourceMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) { - if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element)) { - final JsonArray paymentChannelMappingArray = this.fromApiJsonHelper - .extractJsonArrayNamed(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element); - if (paymentChannelMappingArray != null && paymentChannelMappingArray.size() > 0) { - int i = 0; - do { - final JsonObject jsonObject = paymentChannelMappingArray.get(i).getAsJsonObject(); - final Long paymentTypeId = jsonObject.get(SavingProductAccountingParams.PAYMENT_TYPE.getValue()).getAsLong(); - final Long paymentSpecificFundAccountId = jsonObject.get(SavingProductAccountingParams.FUND_SOURCE.getValue()) - .getAsLong(); - baseDataValidator.reset() - .parameter(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue() + "[" + i + "]." - + SavingProductAccountingParams.PAYMENT_TYPE.toString()) - .value(paymentTypeId).notNull().integerGreaterThanZero(); - baseDataValidator.reset() - .parameter(SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue() + "[" + i + "]." - + SavingProductAccountingParams.FUND_SOURCE.getValue()) - .value(paymentSpecificFundAccountId).notNull().integerGreaterThanZero(); - i++; - } while (i < paymentChannelMappingArray.size()); - } - } - } - - private void validateChargeToIncomeAccountMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) { - // validate for both fee and penalty charges - validateChargeToIncomeAccountMappings(baseDataValidator, element, true); - validateChargeToIncomeAccountMappings(baseDataValidator, element, true); - } - - private void validateChargeToIncomeAccountMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element, - final boolean isPenalty) { - String parameterName; - if (isPenalty) { - parameterName = SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(); - } else { - parameterName = SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(); - } - - if (this.fromApiJsonHelper.parameterExists(parameterName, element)) { - final JsonArray chargeToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element); - if (chargeToIncomeAccountMappingArray != null && chargeToIncomeAccountMappingArray.size() > 0) { - int i = 0; - do { - final JsonObject jsonObject = chargeToIncomeAccountMappingArray.get(i).getAsJsonObject(); - final Long chargeId = this.fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.CHARGE_ID.getValue(), - jsonObject); - final Long incomeAccountId = this.fromApiJsonHelper - .extractLongNamed(SavingProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), jsonObject); - baseDataValidator.reset().parameter(parameterName + "[" + i + "]." + SavingProductAccountingParams.CHARGE_ID.getValue()) - .value(chargeId).notNull().integerGreaterThanZero(); - baseDataValidator.reset() - .parameter(parameterName + "[" + i + "]." + SavingProductAccountingParams.INCOME_ACCOUNT_ID.getValue()) - .value(incomeAccountId).notNull().integerGreaterThanZero(); - i++; - } while (i < chargeToIncomeAccountMappingArray.size()); - } - } - } - private void validateOverdraftParams(final DataValidatorBuilder baseDataValidator, final JsonElement element) { if (this.fromApiJsonHelper.parameterExists(allowOverdraftParamName, element)) { final Boolean allowOverdraft = this.fromApiJsonHelper.extractBooleanNamed(allowOverdraftParamName, element); 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 aca2e099e..47a80859b 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 @@ -46,6 +46,8 @@ import java.util.Locale; import java.util.Map; import java.util.TimeZone; import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse; +import org.apache.fineract.client.models.GetRecurringDepositProductsProductIdResponse; +import org.apache.fineract.client.models.GetSavingsProductsProductIdResponse; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.CollateralManagementHelper; import org.apache.fineract.integrationtests.common.CommonConstants; @@ -141,6 +143,7 @@ public class AccountingScenarioIntegrationTest { this.journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec); this.schedulerJobHelper = new SchedulerJobHelper(requestSpec); this.periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(requestSpec, responseSpec); + this.savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec); this.tenantTimeZone = TimeZone.getTimeZone(Utils.TENANT_TIME_ZONE); } @@ -281,7 +284,6 @@ public class AccountingScenarioIntegrationTest { @Test public void checkAccountingWithSavingsFlow() { - this.savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec); final Account assetAccount = this.accountHelper.createAssetAccount(); final Account incomeAccount = this.accountHelper.createIncomeAccount(); @@ -373,10 +375,107 @@ public class AccountingScenarioIntegrationTest { assertEquals(balance, summary.get("accountBalance"), "Verifying Balance"); } + @Test + public void checkAccountingWithSavingsFlowUsingAccrualAccounting() { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account liabilityAccount = this.accountHelper.createLiabilityAccount(); + + final Integer savingsProductID = createSavingsProductWithAccrualAccounting(MINIMUM_OPENING_BALANCE, assetAccount, incomeAccount, + expenseAccount, liabilityAccount); + final GetSavingsProductsProductIdResponse savingsProductsResponse = SavingsProductHelper.getSavingsProductById(requestSpec, + responseSpec, savingsProductID); + Assertions.assertNotNull(savingsProductsResponse); + Assertions.assertNotNull(savingsProductsResponse.getAccountingMappings()); + Assertions.assertNotNull(savingsProductsResponse.getAccountingMappings().getSavingsControlAccount()); + Assertions.assertNotNull(savingsProductsResponse.getAccountingMappings().getInterestPayableAccount()); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, DATE_OF_JOINING); + final Integer savingsID = this.savingsAccountHelper.applyForSavingsApplication(clientID, savingsProductID, ACCOUNT_TYPE_INDIVIDUAL); + + HashMap savingsStatusHashMap = SavingsStatusChecker.getStatusOfSavings(requestSpec, responseSpec, savingsID); + SavingsStatusChecker.verifySavingsIsPending(savingsStatusHashMap); + + savingsStatusHashMap = this.savingsAccountHelper.approveSavings(savingsID); + SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap); + + savingsStatusHashMap = this.savingsAccountHelper.activateSavings(savingsID); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + + // Checking initial Account entries. + final JournalEntry[] assetAccountInitialEntry = { new JournalEntry(SP_BALANCE, JournalEntry.TransactionType.DEBIT) }; + final JournalEntry[] liablilityAccountInitialEntry = { new JournalEntry(SP_BALANCE, JournalEntry.TransactionType.CREDIT) }; + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, TRANSACTION_DATE, assetAccountInitialEntry); + this.journalEntryHelper.checkJournalEntryForLiabilityAccount(liabilityAccount, TRANSACTION_DATE, liablilityAccountInitialEntry); + + // First Transaction-Deposit + this.savingsAccountHelper.depositToSavingsAccount(savingsID, DEPOSIT_AMOUNT, SavingsAccountHelper.TRANSACTION_DATE, + CommonConstants.RESPONSE_RESOURCE_ID); + Float balance = SP_BALANCE + SP_DEPOSIT_AMOUNT; + HashMap summary = this.savingsAccountHelper.getSavingsSummary(savingsID); + assertEquals(balance, summary.get("accountBalance"), "Verifying Balance after Deposit"); + + LOG.info("----------------------Verifying Journal Entry after the Transaction Deposit----------------------------"); + final JournalEntry[] assetAccountFirstTransactionEntry = { + new JournalEntry(SP_DEPOSIT_AMOUNT, JournalEntry.TransactionType.DEBIT) }; + final JournalEntry[] liabililityAccountFirstTransactionEntry = { + new JournalEntry(SP_DEPOSIT_AMOUNT, JournalEntry.TransactionType.CREDIT) }; + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, TRANSACTION_DATE, assetAccountFirstTransactionEntry); + this.journalEntryHelper.checkJournalEntryForLiabilityAccount(liabilityAccount, TRANSACTION_DATE, + liabililityAccountFirstTransactionEntry); + + // Second Transaction-Withdrawal + this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsID, WITHDRAWAL_AMOUNT, SavingsAccountHelper.TRANSACTION_DATE, + CommonConstants.RESPONSE_RESOURCE_ID); + balance -= SP_WITHDRAWAL_AMOUNT; + summary = this.savingsAccountHelper.getSavingsSummary(savingsID); + assertEquals(balance, summary.get("accountBalance"), "Verifying Balance after Withdrawal"); + + LOG.info("-------------------Verifying Journal Entry after the Transaction Withdrawal----------------------"); + final JournalEntry[] assetAccountSecondTransactionEntry = { + new JournalEntry(SP_WITHDRAWAL_AMOUNT, JournalEntry.TransactionType.CREDIT) }; + final JournalEntry[] liabililityAccountSecondTransactionEntry = { + new JournalEntry(SP_WITHDRAWAL_AMOUNT, JournalEntry.TransactionType.DEBIT) }; + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, TRANSACTION_DATE, assetAccountSecondTransactionEntry); + this.journalEntryHelper.checkJournalEntryForLiabilityAccount(liabilityAccount, TRANSACTION_DATE, + liabililityAccountSecondTransactionEntry); + + // Third Transaction-Add Charges for Withdrawal Fee + final Integer withdrawalChargeId = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getSavingsWithdrawalFeeJSON()); + Assertions.assertNotNull(withdrawalChargeId); + + this.savingsAccountHelper.addChargesForSavings(savingsID, withdrawalChargeId, false); + ArrayList chargesPendingState = this.savingsAccountHelper.getSavingsCharges(savingsID); + Assertions.assertEquals(1, chargesPendingState.size()); + HashMap savingsChargeForPay = chargesPendingState.get(0); + HashMap paidCharge = this.savingsAccountHelper.getSavingsCharge(savingsID, (Integer) savingsChargeForPay.get("id")); + Float chargeAmount = (Float) paidCharge.get("amount"); + + // Withdrawal after adding Charge of type Withdrawal Fee + this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsID, WITHDRAWAL_AMOUNT_ADJUSTED, SavingsAccountHelper.TRANSACTION_DATE, + CommonConstants.RESPONSE_RESOURCE_ID); + summary = this.savingsAccountHelper.getSavingsSummary(savingsID); + balance = balance - SP_WITHDRAWAL_AMOUNT_ADJUSTED - chargeAmount; + + final JournalEntry[] liabililityAccountThirdTransactionEntry = { new JournalEntry(chargeAmount, JournalEntry.TransactionType.DEBIT), + new JournalEntry(SP_WITHDRAWAL_AMOUNT_ADJUSTED, JournalEntry.TransactionType.DEBIT) }; + final JournalEntry[] assetAccountThirdTransactionEntry = { + new JournalEntry(SP_WITHDRAWAL_AMOUNT_ADJUSTED, JournalEntry.TransactionType.CREDIT) }; + final JournalEntry[] incomeAccountThirdTransactionEntry = { new JournalEntry(chargeAmount, JournalEntry.TransactionType.CREDIT) }; + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, TRANSACTION_DATE, assetAccountThirdTransactionEntry); + this.journalEntryHelper.checkJournalEntryForLiabilityAccount(liabilityAccount, TRANSACTION_DATE, + liabililityAccountThirdTransactionEntry); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, TRANSACTION_DATE, incomeAccountThirdTransactionEntry); + + // Verifying Balance after applying Charge for Withdrawal Fee + assertEquals(balance, summary.get("accountBalance"), "Verifying Balance"); + } + @Test public void testFixedDepositAccountingFlow() { this.accountHelper = new AccountHelper(requestSpec, responseSpec); - this.savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec); this.fixedDepositAccountHelper = new FixedDepositAccountHelper(requestSpec, responseSpec); final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US); @@ -481,6 +580,12 @@ public class AccountingScenarioIntegrationTest { Integer recurringDepositProductId = createRecurringDepositProduct(VALID_FROM, VALID_TO, assetAccount, liabilityAccount, incomeAccount, expenseAccount); Assertions.assertNotNull(recurringDepositProductId); + final GetRecurringDepositProductsProductIdResponse recurringDepositProductsProduct = RecurringDepositProductHelper + .getRecurringDepositProductById(requestSpec, responseSpec, recurringDepositProductId); + Assertions.assertNotNull(recurringDepositProductsProduct); + Assertions.assertNotNull(recurringDepositProductsProduct.getAccountingMappings()); + Assertions.assertNotNull(recurringDepositProductsProduct.getAccountingMappings().getSavingsControlAccount()); + Assertions.assertNull(recurringDepositProductsProduct.getAccountingMappings().getInterestPayableAccount()); Integer recurringDepositAccountId = applyForRecurringDepositApplication(clientId.toString(), recurringDepositProductId.toString(), VALID_FROM, VALID_TO, SUBMITTED_ON_DATE, RecurringDepositTest.WHOLE_TERM, EXPECTED_FIRST_DEPOSIT_ON_DATE); @@ -573,6 +678,15 @@ public class AccountingScenarioIntegrationTest { return RecurringDepositAccountHelper.applyRecurringDepositApplication(recurringDepositApplicationJSON, 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); + } + @Test public void checkPeriodicAccrualAccountingFlow() throws InterruptedException, ParseException { final Account assetAccount = this.accountHelper.createAssetAccount(); @@ -1124,7 +1238,6 @@ public class AccountingScenarioIntegrationTest { @Test public void checkAccountingWithSharingFlow() { - this.savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec); final Account assetAccount = this.accountHelper.createAssetAccount(); final Account incomeAccount = this.accountHelper.createIncomeAccount(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/recurringdeposit/RecurringDepositProductHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/recurringdeposit/RecurringDepositProductHelper.java index ca2650a94..73fab66e0 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/recurringdeposit/RecurringDepositProductHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/recurringdeposit/RecurringDepositProductHelper.java @@ -25,6 +25,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.fineract.client.models.GetRecurringDepositProductsProductIdResponse; +import org.apache.fineract.client.util.JSON; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; import org.slf4j.Logger; @@ -36,6 +38,7 @@ public class RecurringDepositProductHelper { private static final Logger LOG = LoggerFactory.getLogger(RecurringDepositProductHelper.class); private final RequestSpecification requestSpec; private final ResponseSpecification responseSpec; + private static final Gson GSON = new JSON().getGson(); public RecurringDepositProductHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { this.requestSpec = requestSpec; @@ -241,6 +244,14 @@ public class RecurringDepositProductHelper { return response; } + public static GetRecurringDepositProductsProductIdResponse getRecurringDepositProductById(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer productId) { + LOG.info("-------------------- RETRIEVING RECURRING DEPOSIT PRODUCT BY ID --------------------------"); + final String GET_RD_PRODUCT_BY_ID_URL = RECURRING_DEPOSIT_PRODUCT_URL + "/" + productId + "?" + Utils.TENANT_IDENTIFIER; + final String response = Utils.performServerGet(requestSpec, responseSpec, GET_RD_PRODUCT_BY_ID_URL); + return GSON.fromJson(response, GetRecurringDepositProductsProductIdResponse.class); + } + public static HashMap retrieveRecurringDepositProductById(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final String productId) { LOG.info("-------------------- RETRIEVING RECURRING DEPOSIT PRODUCT BY ID --------------------------"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java index 64234b53b..a22ba29c1 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java @@ -26,6 +26,8 @@ import io.restassured.specification.ResponseSpecification; import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; +import org.apache.fineract.client.models.GetSavingsProductsProductIdResponse; +import org.apache.fineract.client.util.JSON; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; import org.slf4j.Logger; @@ -37,6 +39,7 @@ public class SavingsProductHelper { private static final Logger LOG = LoggerFactory.getLogger(SavingsProductHelper.class); private static final String SAVINGS_PRODUCT_URL = "/fineract-provider/api/v1/savingsproducts"; private static final String CREATE_SAVINGS_PRODUCT_URL = SAVINGS_PRODUCT_URL + "?" + Utils.TENANT_IDENTIFIER; + private static final Gson GSON = new JSON().getGson(); private static final String LOCALE = "en_GB"; private static final String DIGITS_AFTER_DECIMAL = "4"; @@ -56,6 +59,7 @@ public class SavingsProductHelper { private static final String DAYS_365 = "365"; private static final String NONE = "1"; private static final String CASH_BASED = "2"; + private static final String ACCRUAL_PERIODIC = "3"; private String nameOfSavingsProduct = Utils.uniqueRandomStringGenerator("SAVINGS_PRODUCT_", 6); private String shortName = Utils.uniqueRandomStringGenerator("", 4); @@ -143,6 +147,9 @@ public class SavingsProductHelper { if (this.accountingRule.equals(CASH_BASED)) { map.putAll(getAccountMappingForCashBased()); } + if (this.accountingRule.equals(ACCRUAL_PERIODIC)) { + map.putAll(getAccountMappingForAccrualBased()); + } if (this.isDormancyTrackingActive) { map.put("isDormancyTrackingActive", Boolean.toString(this.isDormancyTrackingActive)); map.put("daysToInactive", this.daysToInactive); @@ -216,6 +223,12 @@ public class SavingsProductHelper { return this; } + public SavingsProductHelper withAccountingRuleAsAccrualBased(final Account[] account_list) { + this.accountingRule = ACCRUAL_PERIODIC; + this.accountList = account_list; + return this; + } + public SavingsProductHelper withAccountingRuleAsCashBased(final Account[] account_list) { this.accountingRule = CASH_BASED; this.accountList = account_list; @@ -306,6 +319,39 @@ public class SavingsProductHelper { return map; } + private Map getAccountMappingForAccrualBased() { + final Map map = new HashMap<>(); + if (accountList != null) { + for (int i = 0; i < this.accountList.length; i++) { + if (this.accountList[i].getAccountType().equals(Account.AccountType.ASSET)) { + final String ID = this.accountList[i].getAccountID().toString(); + map.put("savingsReferenceAccountId", ID); + map.put("overdraftPortfolioControlId", ID); + map.put("feesReceivableAccountId", ID); + map.put("penaltiesReceivableAccountId", ID); + } + if (this.accountList[i].getAccountType().equals(Account.AccountType.LIABILITY)) { + final String ID = this.accountList[i].getAccountID().toString(); + map.put("savingsControlAccountId", ID); + map.put("transfersInSuspenseAccountId", ID); + map.put("interestPayableAccountId", ID); + } + if (this.accountList[i].getAccountType().equals(Account.AccountType.EXPENSE)) { + final String ID = this.accountList[i].getAccountID().toString(); + map.put("interestOnSavingsAccountId", ID); + map.put("writeOffAccountId", ID); + } + if (this.accountList[i].getAccountType().equals(Account.AccountType.INCOME)) { + final String ID = this.accountList[i].getAccountID().toString(); + map.put("incomeFromFeeAccountId", ID); + map.put("incomeFromPenaltyAccountId", ID); + map.put("incomeFromInterestId", ID); + } + } + } + return map; + } + public static Integer createSavingsProduct(final String savingsProductJSON, final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { return Utils.performServerPost(requestSpec, responseSpec, CREATE_SAVINGS_PRODUCT_URL, savingsProductJSON, "resourceId"); @@ -326,4 +372,13 @@ public class SavingsProductHelper { this.daysToEscheat = "90"; return this; } + + public static GetSavingsProductsProductIdResponse getSavingsProductById(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer productId) { + LOG.info("-------------------- RETRIEVING SAVINGS DEPOSIT PRODUCT BY ID --------------------------"); + final String GET_PRODUCT_BY_ID_URL = SAVINGS_PRODUCT_URL + "/" + productId + "?" + Utils.TENANT_IDENTIFIER; + final String response = Utils.performServerGet(requestSpec, responseSpec, GET_PRODUCT_BY_ID_URL); + return GSON.fromJson(response, GetSavingsProductsProductIdResponse.class); + } + }