FINERACT-2312: Adjustment to savings account products by adding a new accounting account interest receivables account. (#4876)

This commit is contained in:
04Gomez
2025-08-20 09:54:18 -06:00
committed by GitHub
parent 1e926669d3
commit c936c19b85
9 changed files with 224 additions and 8 deletions
@@ -100,6 +100,7 @@ public class ProductToGLAccountMappingHelper {
optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditInterestAccountId");
optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditFeesAccountId");
optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditPenaltyAccountId");
optionalProductToGLAccountMappingEntries.add("interestReceivableAccountId");
optionalProductToGLAccountMappingEntries.add(LoanProductAccountingParams.DEFERRED_INCOME_LIABILITY.getValue());
optionalProductToGLAccountMappingEntries.add(LoanProductAccountingParams.INCOME_FROM_CAPITALIZATION.getValue());
optionalProductToGLAccountMappingEntries.add(LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue());
@@ -358,6 +358,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
// Assets
if (glAccountForSavings.equals(AccrualAccountsForSavings.SAVINGS_REFERENCE)) {
accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_REFERENCE.getValue(), glAccountData);
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.INTEREST_RECEIVABLE)) {
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_RECEIVABLE.getValue(), glAccountData);
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL)) {
accountMappingDetails.put(SavingProductAccountingDataParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), glAccountData);
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.FEES_RECEIVABLE)) {
@@ -263,6 +263,10 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
savingsProductId, AccrualAccountsForSavings.FEES_RECEIVABLE.getValue(),
AccrualAccountsForSavings.FEES_RECEIVABLE.toString(), changes);
mergeSavingsToAssetAccountMappingChanges(element, SavingProductAccountingParams.INTEREST_RECEIVABLE.getValue(),
savingsProductId, AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue(),
AccrualAccountsForSavings.INTEREST_RECEIVABLE.toString(), changes);
mergeSavingsToAssetAccountMappingChanges(element, SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(),
savingsProductId, AccrualAccountsForSavings.PENALTIES_RECEIVABLE.getValue(),
AccrualAccountsForSavings.PENALTIES_RECEIVABLE.toString(), changes);
@@ -312,7 +312,8 @@ public final class AccountingConstants {
ESCHEAT_LIABILITY(14), //
FEES_RECEIVABLE(15), //
PENALTIES_RECEIVABLE(16), //
INTEREST_PAYABLE(17);
INTEREST_PAYABLE(17), //
INTEREST_RECEIVABLE(18);
private final Integer value;
@@ -366,6 +367,7 @@ public final class AccountingConstants {
LOSSES_WRITTEN_OFF("writeOffAccountId"), //
ESCHEAT_LIABILITY("escheatLiabilityId"), //
PENALTIES_RECEIVABLE("penaltiesReceivableAccountId"), //
INTEREST_RECEIVABLE("interestReceivableAccountId"), //
FEES_RECEIVABLE("feesReceivableAccountId"), //
INTEREST_PAYABLE("interestPayableAccountId");
@@ -404,7 +406,8 @@ public final class AccountingConstants {
ESCHEAT_LIABILITY("escheatLiabilityAccount"), //
FEES_RECEIVABLE("feeReceivableAccount"), //
PENALTIES_RECEIVABLE("penaltyReceivableAccount"), //
INTEREST_PAYABLE("interestPayableAccount"); //
INTEREST_PAYABLE("interestPayableAccount"), //
INTEREST_RECEIVABLE("interestReceivableAccount"); //
private final String value;
@@ -303,6 +303,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
final JsonElement element = this.fromApiJsonHelper.parse(command.json());
final Integer accountingRuleTypeId = this.fromApiJsonHelper.extractIntegerNamed(accountingRuleParamName, element,
Locale.getDefault());
final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountingRuleTypeId);
switch (accountingRuleType) {
case NONE:
@@ -314,6 +315,10 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
case ACCRUAL_PERIODIC:
saveSavingsBaseAccountMapping(savingProductId, accountType, command, element);
// assets
this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element,
SavingProductAccountingParams.INTEREST_RECEIVABLE.getValue(), savingProductId,
AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue());
this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element,
SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), savingProductId,
AccrualAccountsForSavings.FEES_RECEIVABLE.getValue());
@@ -257,6 +257,7 @@ final class SavingsProductsApiResourceSwagger {
public GetSavingsProductsGlAccount feeReceivableAccount;
public GetSavingsProductsGlAccount penaltyReceivableAccount;
public GetSavingsProductsGlAccount incomeFromFeeAccount;
public GetSavingsProductsGlAccount interestReceivableAccount;
public GetSavingsProductsGlAccount incomeFromPenaltyAccount;
public GetSavingsProductsGlAccount incomeFromInterest;
public GetSavingsProductsGlAccount interestOnSavingsAccount;
@@ -104,12 +104,13 @@ public class SavingsProductDataValidator {
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,
daysToInactiveParamName, daysToEscheatParamName, allowOverdraftParamName, overdraftLimitParamName,
nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName,
SavingsApiConstants.minRequiredBalanceParamName, SavingsApiConstants.enforceMinRequiredBalanceParamName,
SavingsApiConstants.maxAllowedLienLimitParamName, SavingsApiConstants.lienAllowedParamName,
minBalanceForInterestCalculationParamName, withHoldTaxParamName, taxGroupIdParamName));
SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), SavingProductAccountingParams.INTEREST_RECEIVABLE.getValue(),
isDormancyTrackingActiveParamName, daysToDormancyParamName, daysToInactiveParamName, daysToEscheatParamName,
allowOverdraftParamName, overdraftLimitParamName, nominalAnnualInterestRateOverdraftParamName,
minOverdraftForInterestCalculationParamName, SavingsApiConstants.minRequiredBalanceParamName,
SavingsApiConstants.enforceMinRequiredBalanceParamName, SavingsApiConstants.maxAllowedLienLimitParamName,
SavingsApiConstants.lienAllowedParamName, minBalanceForInterestCalculationParamName, withHoldTaxParamName,
taxGroupIdParamName));
public void validateForCreate(final String json) {
@@ -0,0 +1,171 @@
/**
* 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.integrationtests;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import org.apache.fineract.client.models.GetSavingsProductsProductIdResponse;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.Account;
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SavingsProductCreationIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(SavingsProductCreationIntegrationTest.class);
private static ResponseSpecification responseSpec;
private static RequestSpecification requestSpec;
private AccountHelper accountHelper;
private SavingsAccountHelper savingsAccountHelper;
public static final String MINIMUM_OPENING_BALANCE = "1000.0";
@BeforeEach
public void setup() {
Utils.initializeRESTAssured();
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec);
this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
}
@Test
public void testStandardSavingsProductCreation_DoesNotAllowOverdraft() {
// --- ARRANGE ---
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 = createSavingsProductWithAccrualAccountingWithOutOverdraftAllowed(MINIMUM_OPENING_BALANCE,
assetAccount, incomeAccount, expenseAccount, liabilityAccount);
final GetSavingsProductsProductIdResponse savingsProductsResponse = SavingsProductHelper.getSavingsProductById(requestSpec,
responseSpec, savingsProductID);
Assertions.assertNotNull(savingsProductsResponse);
Assertions.assertNotNull(savingsProductsResponse.getAccountingMappings());
Assertions.assertNull(savingsProductsResponse.getAccountingMappings().getInterestReceivableAccount());
}
@Test
public void testSavingsProductWithOverdraftCreation_AllowsOverdraft() {
// --- ARRANGE ---
final Account assetAccount = this.accountHelper.createAssetAccount();
final Account interestReceivableAccount = accountHelper.createAssetAccount("interestReceivableAccount");
final Account incomeAccount = this.accountHelper.createIncomeAccount();
final Account expenseAccount = this.accountHelper.createExpenseAccount();
final Account liabilityAccount = this.accountHelper.createLiabilityAccount();
final Integer savingsProductID = createSavingsProductWithAccrualAccountingWithOverdraftAllowed(
interestReceivableAccount.getAccountID().toString(), 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().getInterestReceivableAccount());
Assertions.assertEquals(interestReceivableAccount.getAccountID(),
savingsProductsResponse.getAccountingMappings().getInterestReceivableAccount().getId().intValue());
}
@Test
public void testSavingsProductWithOverdraftUpdate_AllowsOverdraft() {
// --- ARRANGE ---
final Account assetAccount = this.accountHelper.createAssetAccount();
final Account interestReceivableAccount = accountHelper.createAssetAccount("interestReceivableAccount");
final Account incomeAccount = this.accountHelper.createIncomeAccount();
final Account expenseAccount = this.accountHelper.createExpenseAccount();
final Account liabilityAccount = this.accountHelper.createLiabilityAccount();
final Integer savingsProductID = createSavingsProductWithAccrualAccountingWithOverdraftAllowed(
interestReceivableAccount.getAccountID().toString(), 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().getInterestReceivableAccount());
Assertions.assertEquals(interestReceivableAccount.getAccountID(),
savingsProductsResponse.getAccountingMappings().getInterestReceivableAccount().getId().intValue());
final Account newInterestReceivableAccount = accountHelper.createAssetAccount("interestReceivableAccount");
final Integer savingsProductIDupdate = updateSavingsProductWithAccrualAccountingWithOverdraftAllowed(savingsProductID,
newInterestReceivableAccount.getAccountID().toString(), MINIMUM_OPENING_BALANCE, assetAccount, incomeAccount,
expenseAccount, liabilityAccount);
final GetSavingsProductsProductIdResponse savingsProductsResponseUpdate = SavingsProductHelper.getSavingsProductById(requestSpec,
responseSpec, savingsProductIDupdate);
Assertions.assertNotNull(savingsProductsResponseUpdate);
Assertions.assertNotNull(savingsProductsResponseUpdate.getAccountingMappings());
Assertions.assertNotNull(savingsProductsResponseUpdate.getAccountingMappings().getInterestReceivableAccount());
Assertions.assertEquals(newInterestReceivableAccount.getAccountID(),
savingsProductsResponseUpdate.getAccountingMappings().getInterestReceivableAccount().getId().intValue());
Assertions.assertNotEquals(interestReceivableAccount.getAccountID(),
savingsProductsResponseUpdate.getAccountingMappings().getInterestReceivableAccount().getId().intValue());
}
public static Integer createSavingsProductWithAccrualAccountingWithOverdraftAllowed(final String interestReceivableAccount,
final String minOpenningBalance, final Account... accounts) {
LOG.info("------------------------------CREATING NEW SAVINGS PRODUCT WITH OVERDRAFT ---------------------------------------");
final String savingsProductJSON = new SavingsProductHelper().withInterestCompoundingPeriodTypeAsDaily() //
.withInterestPostingPeriodTypeAsQuarterly() //
.withInterestCalculationPeriodTypeAsDailyBalance() //
.withOverDraft("100000").withAccountInterestReceivables(interestReceivableAccount)
.withMinimumOpenningBalance(minOpenningBalance).withAccountingRuleAsAccrualBased(accounts).build();
return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec);
}
public static Integer createSavingsProductWithAccrualAccountingWithOutOverdraftAllowed(final String minOpenningBalance,
final Account... accounts) {
LOG.info("------------------------------CREATING NEW SAVINGS PRODUCT WITHOUT OVERDRAFT ---------------------------------------");
final String savingsProductJSON = new SavingsProductHelper().withInterestCompoundingPeriodTypeAsDaily() //
.withInterestPostingPeriodTypeAsQuarterly() //
.withInterestCalculationPeriodTypeAsDailyBalance() //
.withMinimumOpenningBalance(minOpenningBalance).withAccountingRuleAsAccrualBased(accounts).build();
return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec);
}
public static Integer updateSavingsProductWithAccrualAccountingWithOverdraftAllowed(final Integer productId,
final String interestReceivableAccount, final String minOpenningBalance, final Account... accounts) {
LOG.info("------------------------------UPDATE SAVINGS PRODUCT ACCOUNT ---------------------------------------");
final String savingsProductJSON = new SavingsProductHelper().withInterestCompoundingPeriodTypeAsDaily() //
.withInterestPostingPeriodTypeAsQuarterly() //
.withInterestCalculationPeriodTypeAsDailyBalance() //
.withOverDraft("100000").withAccountInterestReceivables(interestReceivableAccount)
.withMinimumOpenningBalance(minOpenningBalance).withAccountingRuleAsAccrualBased(accounts).build();
return SavingsProductHelper.updateSavingsProduct(savingsProductJSON, requestSpec, responseSpec, productId);
}
}
@@ -101,6 +101,7 @@ public class SavingsProductHelper {
private Boolean withgsimID = null;
private Integer gsimID = null;
private String nominalAnnualInterestRateOverdraft = null;
private String interestReceivableAccountId = null;
// TODO: Rewrite to use fineract-client instead!
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
@@ -126,6 +127,7 @@ public class SavingsProductHelper {
map.put("transfersInSuspenseAccountId", this.transfersInSuspenseAccountId);
map.put("savingsControlAccountId", this.savingsControlAccountId);
map.put("interestOnSavingsAccountId", this.interestOnSavingsAccountId);
map.put("interestReceivableAccountId", this.interestReceivableAccountId);
map.put("incomeFromFeeAccountId", this.incomeFromFeeAccountId);
map.put("incomeFromPenaltyAccountId", this.incomeFromPenaltyAccountId);
map.put("overdraftPortfolioControlId", this.overdraftPortfolioControlId);
@@ -167,6 +169,10 @@ public class SavingsProductHelper {
return savingsProductCreateJson;
}
public static String urlSavingsUpdate(Integer productId) {
return SAVINGS_PRODUCT_URL + "/" + productId;
}
public SavingsProductHelper withSavingsName(final String savingsName) {
this.nameOfSavingsProduct = savingsName;
return this;
@@ -261,6 +267,11 @@ public class SavingsProductHelper {
return this;
}
public SavingsProductHelper withAccountInterestReceivables(final String interestReceivableAccountId) {
this.interestReceivableAccountId = interestReceivableAccountId;
return this;
}
public SavingsProductHelper withOverDraftRate(final String overdraftLimit, String nominalAnnualInterestRateOverdraft) {
this.allowOverdraft = "true";
this.overdraftLimit = overdraftLimit;
@@ -341,8 +352,18 @@ public class SavingsProductHelper {
map.put("overdraftPortfolioControlId", ID);
map.put("feesReceivableAccountId", ID);
map.put("penaltiesReceivableAccountId", ID);
if (Boolean.parseBoolean(this.allowOverdraft)) {
if (this.interestReceivableAccountId != null) {
map.put("interestReceivableAccountId", this.interestReceivableAccountId);
} else {
map.put("interestReceivableAccountId", ID);
}
} else {
map.put("interestReceivableAccountId", "");
}
}
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);
@@ -373,6 +394,13 @@ public class SavingsProductHelper {
return Utils.performServerPost(requestSpec, responseSpec, CREATE_SAVINGS_PRODUCT_URL, savingsProductJSON, "resourceId");
}
@Deprecated(forRemoval = true)
public static Integer updateSavingsProduct(final String savingsProductJSON, final RequestSpecification requestSpec,
final ResponseSpecification responseSpec, Integer productId) {
return Utils.performServerPut(requestSpec, responseSpec, urlSavingsUpdate(productId) + "?" + Utils.TENANT_IDENTIFIER,
savingsProductJSON, "resourceId");
}
// TODO: Rewrite to use fineract-client instead!
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)