fix(accounting,branchday): 3-stage FD interest posting in scheduler, fix branch day close business date validation
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
This commit is contained in:
+5
-1
@@ -252,7 +252,7 @@ public final class DepositsApiConstants {
|
|||||||
SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(),
|
SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(),
|
||||||
SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(),
|
SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(),
|
||||||
SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), chartsParamName, SavingsApiConstants.withHoldTaxParamName,
|
SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), chartsParamName, SavingsApiConstants.withHoldTaxParamName,
|
||||||
SavingsApiConstants.taxGroupIdParamName));
|
SavingsApiConstants.taxGroupIdParamName, SavingsApiConstants.otherGlMappingsParamName));
|
||||||
|
|
||||||
private static final Set<String> PRECLOSURE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
private static final Set<String> PRECLOSURE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnTypeIdParamName));
|
Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnTypeIdParamName));
|
||||||
@@ -390,6 +390,8 @@ public final class DepositsApiConstants {
|
|||||||
fixedDepositRequestData.add("paymentChannelToFundSourceMappings");
|
fixedDepositRequestData.add("paymentChannelToFundSourceMappings");
|
||||||
fixedDepositRequestData.add("feeToIncomeAccountMappings");
|
fixedDepositRequestData.add("feeToIncomeAccountMappings");
|
||||||
fixedDepositRequestData.add("penaltyToIncomeAccountMappings");
|
fixedDepositRequestData.add("penaltyToIncomeAccountMappings");
|
||||||
|
fixedDepositRequestData.add(SavingsApiConstants.otherGlMappingsParamName);
|
||||||
|
fixedDepositRequestData.add("otherGlMappingsMap");
|
||||||
return fixedDepositRequestData;
|
return fixedDepositRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,6 +503,8 @@ public final class DepositsApiConstants {
|
|||||||
recurringDepositRequestData.add("paymentChannelToFundSourceMappings");
|
recurringDepositRequestData.add("paymentChannelToFundSourceMappings");
|
||||||
recurringDepositRequestData.add("feeToIncomeAccountMappings");
|
recurringDepositRequestData.add("feeToIncomeAccountMappings");
|
||||||
recurringDepositRequestData.add("penaltyToIncomeAccountMappings");
|
recurringDepositRequestData.add("penaltyToIncomeAccountMappings");
|
||||||
|
recurringDepositRequestData.add(SavingsApiConstants.otherGlMappingsParamName);
|
||||||
|
recurringDepositRequestData.add("otherGlMappingsMap");
|
||||||
return recurringDepositRequestData;
|
return recurringDepositRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-24
@@ -27,6 +27,7 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
||||||
import org.apache.fineract.accounting.closure.domain.GLClosureRepository;
|
import org.apache.fineract.accounting.closure.domain.GLClosureRepository;
|
||||||
@@ -83,6 +84,7 @@ import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountTransactionE
|
|||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AccountingProcessorHelper {
|
public class AccountingProcessorHelper {
|
||||||
|
|
||||||
@@ -662,16 +664,25 @@ public class AccountingProcessorHelper {
|
|||||||
final GLAccount payableAccount = Optional
|
final GLAccount payableAccount = Optional
|
||||||
.ofNullable(optionalLinkedGLAccountForSavingsProduct(savingsProductId,
|
.ofNullable(optionalLinkedGLAccountForSavingsProduct(savingsProductId,
|
||||||
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), paymentTypeId))
|
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), paymentTypeId))
|
||||||
.or(() -> Optional.ofNullable(namedSavingsGlAccount(savingsProductId, SAVINGS_INTEREST_PAYABLE_MAPPING)))
|
.or(() -> Optional.ofNullable(namedSavingsGlAccount(savingsProductId,
|
||||||
|
SAVINGS_INTEREST_PAYABLE_MAPPING, "Fixed Deposit Interest Payable", "Interest Payable", "Interest Payable Account")))
|
||||||
.orElse(controlAccount);
|
.orElse(controlAccount);
|
||||||
final GLAccount paidAccount = Optional.ofNullable(namedSavingsGlAccount(savingsProductId, SAVINGS_INTEREST_PAID_MAPPING))
|
final GLAccount paidAccount = Optional.ofNullable(namedSavingsGlAccount(savingsProductId,
|
||||||
|
SAVINGS_INTEREST_PAID_MAPPING, "Fixed Deposit Interest Paid", "Interest Paid", "Interest Paid Account"))
|
||||||
.orElse(payableAccount);
|
.orElse(payableAccount);
|
||||||
|
|
||||||
|
log.info("createInterestPostingJournalEntriesForSavings: savingsProductId={}, payableAccount=[{} - {}], paidAccount=[{} - {}], controlAccount=[{} - {}], interestExpense=[{} - {}], amount={}, isReversal={}",
|
||||||
|
savingsProductId, payableAccount.getId(), payableAccount.getName(), paidAccount.getId(), paidAccount.getName(),
|
||||||
|
controlAccount.getId(), controlAccount.getName(), interestOnSavingsAccount.getId(), interestOnSavingsAccount.getName(),
|
||||||
|
amount, isReversal);
|
||||||
|
|
||||||
// A product that names no payable falls back to Savings Control above. The chain below would
|
// A product that names no payable falls back to Savings Control above. The chain below would
|
||||||
// then open with Dr Savings Control / Cr Interest On Savings and close with its exact
|
// then open with Dr Savings Control / Cr Interest On Savings and close with its exact
|
||||||
// reverse, the two cancelling: the expense never charged and the member never credited. Such
|
// reverse, the two cancelling: the expense never charged and the member never credited. Such
|
||||||
// a product has no liability to draw down, so it keeps the single pair it always posted.
|
// a product has no liability to draw down, so it keeps the single pair it always posted.
|
||||||
if (payableAccount.getId().equals(controlAccount.getId())) {
|
if (payableAccount.getId().equals(controlAccount.getId())) {
|
||||||
|
log.warn("createInterestPostingJournalEntriesForSavings: payableAccount == controlAccount [id={}], posting standard 2-leg entry",
|
||||||
|
payableAccount.getId());
|
||||||
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, controlAccount, savingsId, transactionId, transactionDate,
|
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, controlAccount, savingsId, transactionId, transactionDate,
|
||||||
amount, isReversal);
|
amount, isReversal);
|
||||||
return;
|
return;
|
||||||
@@ -697,28 +708,34 @@ public class AccountingProcessorHelper {
|
|||||||
final boolean payableIsLiability = GLAccountType.LIABILITY.getValue().equals(payableAccount.getType());
|
final boolean payableIsLiability = GLAccountType.LIABILITY.getValue().equals(payableAccount.getType());
|
||||||
final BigDecimal alreadyAccrued = payableBalance(office, payableAccount, currencyCode);
|
final BigDecimal alreadyAccrued = payableBalance(office, payableAccount, currencyCode);
|
||||||
final BigDecimal shortfall = amount.subtract(alreadyAccrued).max(BigDecimal.ZERO);
|
final BigDecimal shortfall = amount.subtract(alreadyAccrued).max(BigDecimal.ZERO);
|
||||||
|
log.info("createInterestPostingJournalEntriesForSavings: alreadyAccrued={}, shortfall={}, payableIsLiability={}, isReversal={}",
|
||||||
|
alreadyAccrued, shortfall, payableIsLiability, isReversal);
|
||||||
if (!isReversal && payableIsLiability && shortfall.compareTo(BigDecimal.ZERO) > 0) {
|
if (!isReversal && payableIsLiability && shortfall.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
// Dr Interest On Savings (the expense) / Cr Payable - the accrual that should have run.
|
log.info("createInterestPostingJournalEntriesForSavings: posting Stage 1 [Dr {} / Cr {} = {}]",
|
||||||
|
interestOnSavingsAccount.getName(), payableAccount.getName(), shortfall);
|
||||||
|
// Stage 1: Dr Interest On Savings (the expense) / Cr Payable - the accrual that should have run.
|
||||||
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, payableAccount, savingsId, transactionId, transactionDate,
|
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, payableAccount, savingsId, transactionId, transactionDate,
|
||||||
shortfall, false);
|
shortfall, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Out of the payable, into the interest-paid account the product names.
|
|
||||||
postSavingsLeg(office, currencyCode, payableAccount, interestOnSavingsAccount, savingsId, transactionId, transactionDate, amount,
|
|
||||||
isReversal);
|
|
||||||
|
|
||||||
// A separate paid account, where the product names one, sits between interest-paid and the
|
|
||||||
// member; otherwise the payout leaves straight from interest-paid.
|
|
||||||
final boolean hasSeparatePaidAccount = !payableAccount.getId().equals(paidAccount.getId());
|
final boolean hasSeparatePaidAccount = !payableAccount.getId().equals(paidAccount.getId());
|
||||||
if (hasSeparatePaidAccount) {
|
if (hasSeparatePaidAccount) {
|
||||||
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, paidAccount, savingsId, transactionId, transactionDate, amount,
|
log.info("createInterestPostingJournalEntriesForSavings: posting Stage 2 [Dr {} / Cr {} = {}] and Stage 3 [Dr {} / Cr {} = {}]",
|
||||||
|
payableAccount.getName(), paidAccount.getName(), amount, paidAccount.getName(), controlAccount.getName(), amount);
|
||||||
|
// Stage 2: Out of the payable, into the interest-paid transit/clearing account
|
||||||
|
postSavingsLeg(office, currencyCode, payableAccount, paidAccount, savingsId, transactionId, transactionDate, amount,
|
||||||
|
isReversal);
|
||||||
|
|
||||||
|
// Stage 3: Out of the interest-paid account, into the member's control account
|
||||||
|
postSavingsLeg(office, currencyCode, paidAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
||||||
|
isReversal);
|
||||||
|
} else {
|
||||||
|
log.info("createInterestPostingJournalEntriesForSavings: no separate paidAccount, posting direct payout [Dr {} / Cr {} = {}]",
|
||||||
|
payableAccount.getName(), controlAccount.getName(), amount);
|
||||||
|
// Payout directly from payable into member's control account
|
||||||
|
postSavingsLeg(office, currencyCode, payableAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
||||||
isReversal);
|
isReversal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Into the member's own balance.
|
|
||||||
final GLAccount payoutFromAccount = hasSeparatePaidAccount ? paidAccount : interestOnSavingsAccount;
|
|
||||||
postSavingsLeg(office, currencyCode, payoutFromAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
|
||||||
isReversal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -792,15 +809,14 @@ public class AccountingProcessorHelper {
|
|||||||
return balance == null ? BigDecimal.ZERO : balance;
|
return balance == null ? BigDecimal.ZERO : balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GLAccount namedSavingsGlAccount(final Long savingsProductId, final String accountName) {
|
private GLAccount namedSavingsGlAccount(final Long savingsProductId, final String... accountNames) {
|
||||||
if (savingsProductId == null) {
|
if (savingsProductId == null || accountNames == null || accountNames.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// The column holds an empty string on a product saved with no named mappings, and casting
|
for (final String accountName : accountNames) {
|
||||||
// that to jsonb is an error rather than an empty document - so the cast is done inside a
|
if (accountName == null) {
|
||||||
// subquery that has already filtered those rows out. Postgres is free to evaluate a WHERE
|
continue;
|
||||||
// clause in any order, so guarding at the same query level as the cast would only work by
|
}
|
||||||
// luck.
|
|
||||||
final List<Long> found = jdbcTemplate.queryForList("""
|
final List<Long> found = jdbcTemplate.queryForList("""
|
||||||
SELECT gl.id
|
SELECT gl.id
|
||||||
FROM (SELECT other_gl_mappings::jsonb AS doc
|
FROM (SELECT other_gl_mappings::jsonb AS doc
|
||||||
@@ -816,10 +832,14 @@ public class AccountingProcessorHelper {
|
|||||||
|| COALESCE(sp.doc -> 'income', '[]'::jsonb)
|
|| COALESCE(sp.doc -> 'income', '[]'::jsonb)
|
||||||
) AS m(entry)
|
) AS m(entry)
|
||||||
JOIN acc_gl_account gl ON gl.gl_code = m.entry ->> 'glCode'
|
JOIN acc_gl_account gl ON gl.gl_code = m.entry ->> 'glCode'
|
||||||
WHERE m.entry ->> 'accountName' = ?
|
WHERE lower(trim(m.entry ->> 'accountName')) = lower(trim(?))
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""", Long.class, savingsProductId, accountName);
|
""", Long.class, savingsProductId, accountName);
|
||||||
return found.isEmpty() ? null : glAccountRepository.findById(found.get(0)).orElse(null);
|
if (!found.isEmpty()) {
|
||||||
|
return glAccountRepository.findById(found.get(0)).orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+34
-1
@@ -19,10 +19,13 @@
|
|||||||
package org.apache.fineract.portfolio.savings.service;
|
package org.apache.fineract.portfolio.savings.service;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.accounting.common.AccountingEnumerations;
|
import org.apache.fineract.accounting.common.AccountingEnumerations;
|
||||||
@@ -202,6 +205,31 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final Type OTHER_GL_MAPPINGS_TYPE = new TypeToken<List<Map<String, Object>>>() {}.getType();
|
||||||
|
private static final Type OTHER_GL_MAPPINGS_MAP_TYPE = new TypeToken<Map<String, List<Map<String, Object>>>>() {}.getType();
|
||||||
|
|
||||||
|
private static List<Map<String, Object>> parseOtherGlMappings(final String otherGlMappingsJson) {
|
||||||
|
if (otherGlMappingsJson == null || otherGlMappingsJson.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
com.google.gson.JsonElement element = GSON.fromJson(otherGlMappingsJson, com.google.gson.JsonElement.class);
|
||||||
|
if (!element.isJsonArray()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GSON.fromJson(element, OTHER_GL_MAPPINGS_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, List<Map<String, Object>>> parseOtherGlMappingsAsMap(final String otherGlMappingsJson) {
|
||||||
|
if (otherGlMappingsJson == null || otherGlMappingsJson.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
com.google.gson.JsonElement element = GSON.fromJson(otherGlMappingsJson, com.google.gson.JsonElement.class);
|
||||||
|
if (!element.isJsonObject()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return GSON.fromJson(element, OTHER_GL_MAPPINGS_MAP_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
private abstract static class DepositProductMapper implements RowMapper<DepositProductData> {
|
private abstract static class DepositProductMapper implements RowMapper<DepositProductData> {
|
||||||
|
|
||||||
private final String schemaSql;
|
private final String schemaSql;
|
||||||
@@ -221,6 +249,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
|||||||
sqlBuilder.append("sp.lockin_period_frequency as lockinPeriodFrequency,");
|
sqlBuilder.append("sp.lockin_period_frequency as lockinPeriodFrequency,");
|
||||||
sqlBuilder.append("sp.lockin_period_frequency_enum as lockinPeriodFrequencyType, ");
|
sqlBuilder.append("sp.lockin_period_frequency_enum as lockinPeriodFrequencyType, ");
|
||||||
sqlBuilder.append("sp.accounting_type as accountingType, ");
|
sqlBuilder.append("sp.accounting_type as accountingType, ");
|
||||||
|
sqlBuilder.append("sp.other_gl_mappings as otherGlMappings, ");
|
||||||
sqlBuilder.append("sp.min_balance_for_interest_calculation as minBalanceForInterestCalculation, ");
|
sqlBuilder.append("sp.min_balance_for_interest_calculation as minBalanceForInterestCalculation, ");
|
||||||
sqlBuilder.append("sp.withhold_tax as withHoldTax,");
|
sqlBuilder.append("sp.withhold_tax as withHoldTax,");
|
||||||
sqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName ");
|
sqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName ");
|
||||||
@@ -284,10 +313,14 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
|||||||
taxGroupData = TaxGroupData.lookup(taxGroupId, taxGroupName);
|
taxGroupData = TaxGroupData.lookup(taxGroupId, taxGroupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DepositProductData.instance(id, name, shortName, description, currency, nominalAnnualInterestRate,
|
final DepositProductData data = DepositProductData.instance(id, name, shortName, description, currency, nominalAnnualInterestRate,
|
||||||
compoundingInterestPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType,
|
compoundingInterestPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType,
|
||||||
lockinPeriodFrequency, lockinPeriodFrequencyType, accountingRuleType, minBalanceForInterestCalculation, withHoldTax,
|
lockinPeriodFrequency, lockinPeriodFrequencyType, accountingRuleType, minBalanceForInterestCalculation, withHoldTax,
|
||||||
taxGroupData);
|
taxGroupData);
|
||||||
|
final String otherGlMappingsJson = rs.getString("otherGlMappings");
|
||||||
|
data.setOtherGlMappings(parseOtherGlMappings(otherGlMappingsJson));
|
||||||
|
data.setOtherGlMappingsMap(parseOtherGlMappingsAsMap(otherGlMappingsJson));
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
@@ -58,6 +58,9 @@ public class DepositProductData {
|
|||||||
|
|
||||||
// accounting
|
// accounting
|
||||||
protected final EnumOptionData accountingRule;
|
protected final EnumOptionData accountingRule;
|
||||||
|
|
||||||
|
private List<Map<String, Object>> otherGlMappings;
|
||||||
|
private Map<String, List<Map<String, Object>>> otherGlMappingsMap;
|
||||||
protected final Map<String, Object> accountingMappings;
|
protected final Map<String, Object> accountingMappings;
|
||||||
protected final Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
|
protected final Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
|
||||||
protected final Collection<ChargeToGLAccountMapper> feeToIncomeAccountMappings;
|
protected final Collection<ChargeToGLAccountMapper> feeToIncomeAccountMappings;
|
||||||
@@ -453,4 +456,20 @@ public class DepositProductData {
|
|||||||
public EnumOptionData getInterestCompoundingPeriodType() {
|
public EnumOptionData getInterestCompoundingPeriodType() {
|
||||||
return interestCompoundingPeriodType;
|
return interestCompoundingPeriodType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getOtherGlMappings() {
|
||||||
|
return this.otherGlMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtherGlMappings(List<Map<String, Object>> otherGlMappings) {
|
||||||
|
this.otherGlMappings = otherGlMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, List<Map<String, Object>>> getOtherGlMappingsMap() {
|
||||||
|
return this.otherGlMappingsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtherGlMappingsMap(Map<String, List<Map<String, Object>>> otherGlMappingsMap) {
|
||||||
|
this.otherGlMappingsMap = otherGlMappingsMap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -244,7 +244,7 @@ public final class FixedDepositProductData extends DepositProductData {
|
|||||||
final Collection<EnumOptionData> periodFrequencyTypeOptions = null;
|
final Collection<EnumOptionData> periodFrequencyTypeOptions = null;
|
||||||
final Collection<TaxGroupData> taxGroupOptions = null;
|
final Collection<TaxGroupData> taxGroupOptions = null;
|
||||||
|
|
||||||
return new FixedDepositProductData(depositProductData.id, depositProductData.name, depositProductData.shortName,
|
final FixedDepositProductData result = new FixedDepositProductData(depositProductData.id, depositProductData.name, depositProductData.shortName,
|
||||||
depositProductData.description, depositProductData.currency, depositProductData.nominalAnnualInterestRate,
|
depositProductData.description, depositProductData.currency, depositProductData.nominalAnnualInterestRate,
|
||||||
depositProductData.interestCompoundingPeriodType, depositProductData.interestPostingPeriodType,
|
depositProductData.interestCompoundingPeriodType, depositProductData.interestPostingPeriodType,
|
||||||
depositProductData.interestCalculationType, depositProductData.interestCalculationDaysInYearType,
|
depositProductData.interestCalculationType, depositProductData.interestCalculationDaysInYearType,
|
||||||
@@ -258,6 +258,9 @@ public final class FixedDepositProductData extends DepositProductData {
|
|||||||
minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm,
|
minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm,
|
||||||
inMultiplesOfDepositTermType, minDepositAmount, depositAmount, maxDepositAmount, periodFrequencyTypeOptions,
|
inMultiplesOfDepositTermType, minDepositAmount, depositAmount, maxDepositAmount, periodFrequencyTypeOptions,
|
||||||
depositProductData.withHoldTax, depositProductData.taxGroup, taxGroupOptions);
|
depositProductData.withHoldTax, depositProductData.taxGroup, taxGroupOptions);
|
||||||
|
result.setOtherGlMappings(depositProductData.getOtherGlMappings());
|
||||||
|
result.setOtherGlMappingsMap(depositProductData.getOtherGlMappingsMap());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FixedDepositProductData lookup(final Long id, final String name) {
|
public static FixedDepositProductData lookup(final Long id, final String name) {
|
||||||
@@ -609,6 +612,8 @@ public final class FixedDepositProductData extends DepositProductData {
|
|||||||
this.balanceFrequency = src.balanceFrequency;
|
this.balanceFrequency = src.balanceFrequency;
|
||||||
this.withdrawalFeeForTransfers = src.withdrawalFeeForTransfers;
|
this.withdrawalFeeForTransfers = src.withdrawalFeeForTransfers;
|
||||||
this.renewalGracePeriodDays = src.renewalGracePeriodDays;
|
this.renewalGracePeriodDays = src.renewalGracePeriodDays;
|
||||||
|
this.setOtherGlMappings(src.getOtherGlMappings());
|
||||||
|
this.setOtherGlMappingsMap(src.getOtherGlMappingsMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Boolean withdrawalFeeForTransfers;
|
private Boolean withdrawalFeeForTransfers;
|
||||||
|
|||||||
+9
@@ -52,6 +52,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPe
|
|||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minBalanceForInterestCalculationParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minBalanceForInterestCalculationParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nameParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nameParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.otherGlMappingsParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shortNameParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shortNameParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName;
|
||||||
@@ -247,6 +248,10 @@ public class DepositProductAssembler {
|
|||||||
fixedDepositProduct.setWithdrawalFeeApplicableForTransfer(command.booleanPrimitiveValueOfParameterNamed("withdrawalFeeForTransfers"));
|
fixedDepositProduct.setWithdrawalFeeApplicableForTransfer(command.booleanPrimitiveValueOfParameterNamed("withdrawalFeeForTransfers"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.hasParameter(otherGlMappingsParamName)) {
|
||||||
|
fixedDepositProduct.setOtherGlMappings(command.jsonFragment(otherGlMappingsParamName));
|
||||||
|
}
|
||||||
|
|
||||||
fixedDepositProduct.validateDomainRules();
|
fixedDepositProduct.validateDomainRules();
|
||||||
|
|
||||||
return fixedDepositProduct;
|
return fixedDepositProduct;
|
||||||
@@ -402,6 +407,10 @@ public class DepositProductAssembler {
|
|||||||
recurringDepositProduct.setWithdrawalFeeApplicableForTransfer(command.booleanPrimitiveValueOfParameterNamed("withdrawalFeeForTransfers"));
|
recurringDepositProduct.setWithdrawalFeeApplicableForTransfer(command.booleanPrimitiveValueOfParameterNamed("withdrawalFeeForTransfers"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.hasParameter(otherGlMappingsParamName)) {
|
||||||
|
recurringDepositProduct.setOtherGlMappings(command.jsonFragment(otherGlMappingsParamName));
|
||||||
|
}
|
||||||
|
|
||||||
recurringDepositProduct.validateDomainRules();
|
recurringDepositProduct.validateDomainRules();
|
||||||
|
|
||||||
return recurringDepositProduct;
|
return recurringDepositProduct;
|
||||||
|
|||||||
+4
@@ -1123,6 +1123,10 @@ public class SavingsProduct extends AbstractPersistableCustom<Long> {
|
|||||||
return this.otherGlMappings;
|
return this.otherGlMappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOtherGlMappings(final String otherGlMappings) {
|
||||||
|
this.otherGlMappings = otherGlMappings;
|
||||||
|
}
|
||||||
|
|
||||||
// Enhanced Savings Product Getters and Setters
|
// Enhanced Savings Product Getters and Setters
|
||||||
public String getAccountType() {
|
public String getAccountType() {
|
||||||
return accountType;
|
return accountType;
|
||||||
|
|||||||
+137
-19
@@ -30,6 +30,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
@@ -96,6 +97,63 @@ public class SavingsSchedularInterestPoster {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final Map<String, Long> namedGlAccountCache = new HashMap<>();
|
||||||
|
|
||||||
|
private Long findNamedGlAccount(final Long savingsProductId, final String... accountNames) {
|
||||||
|
if (savingsProductId == null || accountNames == null || accountNames.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (final String accountName : accountNames) {
|
||||||
|
if (accountName == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String cacheKey = savingsProductId + ":" + accountName.trim().toLowerCase();
|
||||||
|
if (this.namedGlAccountCache.containsKey(cacheKey)) {
|
||||||
|
final Long cached = this.namedGlAccountCache.get(cacheKey);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final List<Long> found = this.jdbcTemplate.queryForList("""
|
||||||
|
SELECT gl.id
|
||||||
|
FROM (SELECT other_gl_mappings::jsonb AS doc
|
||||||
|
FROM m_savings_product
|
||||||
|
WHERE id = ?
|
||||||
|
AND other_gl_mappings IS NOT NULL
|
||||||
|
AND other_gl_mappings <> ''
|
||||||
|
AND other_gl_mappings LIKE '{%') sp
|
||||||
|
CROSS JOIN LATERAL jsonb_array_elements(
|
||||||
|
COALESCE(sp.doc -> 'assets', '[]'::jsonb)
|
||||||
|
|| COALESCE(sp.doc -> 'liabilities', '[]'::jsonb)
|
||||||
|
|| COALESCE(sp.doc -> 'expenses', '[]'::jsonb)
|
||||||
|
|| COALESCE(sp.doc -> 'income', '[]'::jsonb)
|
||||||
|
) AS m(entry)
|
||||||
|
JOIN acc_gl_account gl ON gl.gl_code = m.entry ->> 'glCode'
|
||||||
|
WHERE lower(trim(m.entry ->> 'accountName')) = lower(trim(?))
|
||||||
|
LIMIT 1
|
||||||
|
""", Long.class, savingsProductId, accountName);
|
||||||
|
if (!found.isEmpty()) {
|
||||||
|
this.namedGlAccountCache.put(cacheKey, found.get(0));
|
||||||
|
return found.get(0);
|
||||||
|
} else {
|
||||||
|
this.namedGlAccountCache.put(cacheKey, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addJournalEntryLeg(final List<Object[]> params, final Long accountId, final Long officeId,
|
||||||
|
final String currencyCode, final String transactionId, final Long savingsTransactionId,
|
||||||
|
final LocalDate transactionDate, final JournalEntryType entryType, final BigDecimal amount,
|
||||||
|
final Long savingsAccountId, final OffsetDateTime auditDatetime, final Long userId,
|
||||||
|
final LocalDate businessDate) {
|
||||||
|
params.add(new Object[] { accountId, officeId, null, currencyCode, transactionId,
|
||||||
|
savingsTransactionId, null, false, null, false, transactionDate, entryType.getValue().longValue(),
|
||||||
|
amount, null, entryType.getValue().longValue(), savingsAccountId, auditDatetime, auditDatetime, false,
|
||||||
|
BigDecimal.ZERO, BigDecimal.ZERO, null, transactionDate, null, userId, userId, businessDate });
|
||||||
|
}
|
||||||
|
|
||||||
private void batchUpdateJournalEntries(final List<SavingsAccountData> savingsAccountDataList,
|
private void batchUpdateJournalEntries(final List<SavingsAccountData> savingsAccountDataList,
|
||||||
final HashMap<String, SavingsAccountTransactionData> savingsAccountTransactionDataHashMap)
|
final HashMap<String, SavingsAccountTransactionData> savingsAccountTransactionDataHashMap)
|
||||||
throws DataAccessException, NullPointerException {
|
throws DataAccessException, NullPointerException {
|
||||||
@@ -114,26 +172,86 @@ public class SavingsSchedularInterestPoster {
|
|||||||
savingsAccountTransactionData.setId(dataFromFetch.getId());
|
savingsAccountTransactionData.setId(dataFromFetch.getId());
|
||||||
if (savingsAccountData.getGlAccountIdForSavingsControl() != 0
|
if (savingsAccountData.getGlAccountIdForSavingsControl() != 0
|
||||||
&& savingsAccountData.getGlAccountIdForInterestOnSavings() != 0) {
|
&& savingsAccountData.getGlAccountIdForInterestOnSavings() != 0) {
|
||||||
OffsetDateTime auditDatetime = DateUtils.getAuditOffsetDateTime();
|
final OffsetDateTime auditDatetime = DateUtils.getAuditOffsetDateTime();
|
||||||
paramsForGLInsertion.add(
|
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||||
new Object[] { savingsAccountTransactionData.getAccountCredit(), savingsAccountData.getOfficeId(), null,
|
final String txnIdentifier = SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString();
|
||||||
currencyCode, SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(),
|
final Long savingsTxnId = savingsAccountTransactionData.getId();
|
||||||
savingsAccountTransactionData.getId(), null, false, null, false,
|
final LocalDate txnDate = savingsAccountTransactionData.getTransactionDate();
|
||||||
savingsAccountTransactionData.getTransactionDate(), JournalEntryType.CREDIT.getValue().longValue(),
|
final BigDecimal amount = savingsAccountTransactionData.getAmount();
|
||||||
savingsAccountTransactionData.getAmount(), null, JournalEntryType.CREDIT.getValue().longValue(),
|
final Long savingsAccountId = savingsAccountData.getId();
|
||||||
savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO,
|
final Long officeId = savingsAccountData.getOfficeId();
|
||||||
null, savingsAccountTransactionData.getTransactionDate(), null, userId, userId,
|
|
||||||
DateUtils.getBusinessLocalDate() });
|
|
||||||
|
|
||||||
paramsForGLInsertion
|
if (Boolean.TRUE.equals(isOverdraft)) {
|
||||||
.add(new Object[] { savingsAccountTransactionData.getAccountDebit(), savingsAccountData.getOfficeId(), null,
|
addJournalEntryLeg(paramsForGLInsertion, savingsAccountTransactionData.getAccountCredit(), officeId,
|
||||||
currencyCode, SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(),
|
currencyCode, txnIdentifier, savingsTxnId, txnDate, JournalEntryType.CREDIT, amount,
|
||||||
savingsAccountTransactionData.getId(), null, false, null, false,
|
savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
savingsAccountTransactionData.getTransactionDate(), JournalEntryType.DEBIT.getValue().longValue(),
|
addJournalEntryLeg(paramsForGLInsertion, savingsAccountTransactionData.getAccountDebit(), officeId,
|
||||||
savingsAccountTransactionData.getAmount(), null, JournalEntryType.DEBIT.getValue().longValue(),
|
currencyCode, txnIdentifier, savingsTxnId, txnDate, JournalEntryType.DEBIT, amount,
|
||||||
savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO,
|
savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
null, savingsAccountTransactionData.getTransactionDate(), null, userId, userId,
|
} else {
|
||||||
DateUtils.getBusinessLocalDate() });
|
final Long expenseAccountId = savingsAccountData.getGlAccountIdForInterestOnSavings();
|
||||||
|
final Long controlAccountId = savingsAccountData.getGlAccountIdForSavingsControl();
|
||||||
|
Long payableAccountId = savingsAccountData.getGlAccountIdForInterestPayable();
|
||||||
|
final Long productId = savingsAccountData.getSavingsProductId() != null
|
||||||
|
? savingsAccountData.getSavingsProductId()
|
||||||
|
: savingsAccountData.getProductId();
|
||||||
|
|
||||||
|
if (payableAccountId == null || payableAccountId == 0) {
|
||||||
|
payableAccountId = findNamedGlAccount(productId,
|
||||||
|
"Fixed Deposit Interest Payable", "Savings Interest Payable", "Recurring Deposit Interest Payable",
|
||||||
|
"Interest Payable", "Interest Payable Account");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Long paidAccountId = findNamedGlAccount(productId,
|
||||||
|
"Fixed Deposit Interest Paid", "Savings Interest Paid", "Recurring Deposit Interest Paid",
|
||||||
|
"Interest Paid", "Interest Paid Account");
|
||||||
|
|
||||||
|
final boolean hasPayable = payableAccountId != null && payableAccountId != 0 && !payableAccountId.equals(controlAccountId);
|
||||||
|
final boolean hasPaid = paidAccountId != null && paidAccountId != 0 && !paidAccountId.equals(payableAccountId);
|
||||||
|
|
||||||
|
log.info("SavingsSchedularInterestPoster: accountId={}, productId={}, expense={}, payable={}, paid={}, control={}, amount={}",
|
||||||
|
savingsAccountId, productId, expenseAccountId, payableAccountId, paidAccountId, controlAccountId, amount);
|
||||||
|
|
||||||
|
if (hasPayable && hasPaid) {
|
||||||
|
// Stage 1: Dr Interest Expense / Cr Interest Payable
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, expenseAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.DEBIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, payableAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.CREDIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
|
||||||
|
// Stage 2: Dr Interest Payable / Cr Interest Paid
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, payableAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.DEBIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, paidAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.CREDIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
|
||||||
|
// Stage 3: Dr Interest Paid / Cr Savings Control
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, paidAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.DEBIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, controlAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.CREDIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
} else if (hasPayable) {
|
||||||
|
// Stage 1: Dr Interest Expense / Cr Interest Payable
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, expenseAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.DEBIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, payableAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.CREDIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
|
||||||
|
// Stage 2: Dr Interest Payable / Cr Savings Control
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, payableAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.DEBIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, controlAccountId, officeId, currencyCode, txnIdentifier,
|
||||||
|
savingsTxnId, txnDate, JournalEntryType.CREDIT, amount, savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
} else {
|
||||||
|
// Standard 2-leg entry: Dr Interest Expense / Cr Savings Control
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, savingsAccountTransactionData.getAccountCredit(), officeId,
|
||||||
|
currencyCode, txnIdentifier, savingsTxnId, txnDate, JournalEntryType.CREDIT, amount,
|
||||||
|
savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
addJournalEntryLeg(paramsForGLInsertion, savingsAccountTransactionData.getAccountDebit(), officeId,
|
||||||
|
currencyCode, txnIdentifier, savingsTxnId, txnDate, JournalEntryType.DEBIT, amount,
|
||||||
|
savingsAccountId, auditDatetime, userId, businessDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user