Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd827dec27 | ||
|
|
aa264097e7 | ||
|
|
16c67f6701 |
+2
@@ -477,6 +477,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
|
|||||||
accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), glAccountData);
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.TRANSFERS_SUSPENSE)) {
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.TRANSFERS_SUSPENSE)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), glAccountData);
|
||||||
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_PAYABLE)) {
|
||||||
|
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_PAYABLE.getValue(), glAccountData);
|
||||||
// Income
|
// Income
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_FEES)) {
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_FEES)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), glAccountData);
|
||||||
|
|||||||
+1
@@ -270,6 +270,7 @@ public final class AccountingConstants {
|
|||||||
INCOME_FROM_INTEREST(12), //
|
INCOME_FROM_INTEREST(12), //
|
||||||
LOSSES_WRITTEN_OFF(13), //
|
LOSSES_WRITTEN_OFF(13), //
|
||||||
ESCHEAT_LIABILITY(14), //
|
ESCHEAT_LIABILITY(14), //
|
||||||
|
INTEREST_PAYABLE(17), //
|
||||||
/**
|
/**
|
||||||
* Where the paise dropped from a cash closure are absorbed.
|
* Where the paise dropped from a cash closure are absorbed.
|
||||||
*
|
*
|
||||||
|
|||||||
+14
-1
@@ -32,7 +32,8 @@ public enum DepositAccountOnClosureType {
|
|||||||
TRANSFER_TO_SAVINGS(200, "depositAccountClosureType.transferToSavings"), //
|
TRANSFER_TO_SAVINGS(200, "depositAccountClosureType.transferToSavings"), //
|
||||||
REINVEST_PRINCIPAL_AND_INTEREST(300, "depositAccountClosureType.reinvestPrincipalAndInterest"), //
|
REINVEST_PRINCIPAL_AND_INTEREST(300, "depositAccountClosureType.reinvestPrincipalAndInterest"), //
|
||||||
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"), //
|
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"), //
|
||||||
PARTIAL_REINVEST(500, "depositAccountClosureType.partialReinvest"); //
|
PARTIAL_REINVEST(500, "depositAccountClosureType.partialReinvest"), //
|
||||||
|
TRANSFER_TO_GL(600, "depositAccountClosureType.transferToGl"); //
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
private final String code;
|
private final String code;
|
||||||
@@ -53,6 +54,7 @@ public enum DepositAccountOnClosureType {
|
|||||||
case 300 -> REINVEST_PRINCIPAL_AND_INTEREST;
|
case 300 -> REINVEST_PRINCIPAL_AND_INTEREST;
|
||||||
case 400 -> REINVEST_PRINCIPAL_ONLY;
|
case 400 -> REINVEST_PRINCIPAL_ONLY;
|
||||||
case 500 -> PARTIAL_REINVEST;
|
case 500 -> PARTIAL_REINVEST;
|
||||||
|
case 600 -> TRANSFER_TO_GL;
|
||||||
default -> INVALID;
|
default -> INVALID;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -91,6 +93,17 @@ public enum DepositAccountOnClosureType {
|
|||||||
return this.equals(PARTIAL_REINVEST);
|
return this.equals(PARTIAL_REINVEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pays the closure proceeds into a GL account the maker picks, instead of a savings account or cash.
|
||||||
|
* Only the fixed-deposit premature-closure handler implements it -- see
|
||||||
|
* DepositAccountDomainServiceJpa#handleFDAccountPreMatureClosure; every other closure path rejects it
|
||||||
|
* in DepositAccountTransactionDataValidator#validateClosing rather than falling through to the
|
||||||
|
* terminal "pay the balance out in cash" branch.
|
||||||
|
*/
|
||||||
|
public boolean isTransferToGl() {
|
||||||
|
return this.equals(TRANSFER_TO_GL);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: do we really need this?!?
|
// TODO: do we really need this?!?
|
||||||
public static Object[] integerValues() {
|
public static Object[] integerValues() {
|
||||||
return Arrays.stream(values()).filter(value -> !INVALID.equals(value)).map(value -> value.value).toList().toArray();
|
return Arrays.stream(values()).filter(value -> !INVALID.equals(value)).map(value -> value.value).toList().toArray();
|
||||||
|
|||||||
+8
-1
@@ -212,6 +212,9 @@ public final class DepositsApiConstants {
|
|||||||
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
||||||
public static final String transferDescriptionParamName = "transferDescription";
|
public static final String transferDescriptionParamName = "transferDescription";
|
||||||
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
||||||
|
// Destination GL account for DepositAccountOnClosureType.TRANSFER_TO_GL -- the closure proceeds are
|
||||||
|
// credited straight to this GL instead of a savings account or cash.
|
||||||
|
public static final String toGlAccountIdParamName = "toGlAccountId";
|
||||||
public static final String reinvestAmountParamName = "reinvestAmount";
|
public static final String reinvestAmountParamName = "reinvestAmount";
|
||||||
// Lets a maker waive the product's configured pre-closure penalty for one premature closure --
|
// Lets a maker waive the product's configured pre-closure penalty for one premature closure --
|
||||||
// see FixedDepositAccount#waivePreClosurePenaltyForThisRequest for why this is a transient
|
// see FixedDepositAccount#waivePreClosurePenaltyForThisRequest for why this is a transient
|
||||||
@@ -249,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));
|
||||||
@@ -387,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
@@ -801,6 +801,10 @@ public final class SavingsEnumerations {
|
|||||||
optionData = new EnumOptionData(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue().longValue(),
|
optionData = new EnumOptionData(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue().longValue(),
|
||||||
DepositAccountOnClosureType.PARTIAL_REINVEST.getCode(), "Partial Re-Invest");
|
DepositAccountOnClosureType.PARTIAL_REINVEST.getCode(), "Partial Re-Invest");
|
||||||
break;
|
break;
|
||||||
|
case TRANSFER_TO_GL:
|
||||||
|
optionData = new EnumOptionData(DepositAccountOnClosureType.TRANSFER_TO_GL.getValue().longValue(),
|
||||||
|
DepositAccountOnClosureType.TRANSFER_TO_GL.getCode(), "Transfer to GL Account");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return optionData;
|
return optionData;
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-41
@@ -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,34 +809,37 @@ 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
|
FROM m_savings_product
|
||||||
FROM m_savings_product
|
WHERE id = ?
|
||||||
WHERE id = ?
|
AND other_gl_mappings IS NOT NULL
|
||||||
AND other_gl_mappings IS NOT NULL
|
AND other_gl_mappings <> ''
|
||||||
AND other_gl_mappings <> ''
|
AND other_gl_mappings LIKE '{%') sp
|
||||||
AND other_gl_mappings LIKE '{%') sp
|
CROSS JOIN LATERAL jsonb_array_elements(
|
||||||
CROSS JOIN LATERAL jsonb_array_elements(
|
COALESCE(sp.doc -> 'assets', '[]'::jsonb)
|
||||||
COALESCE(sp.doc -> 'assets', '[]'::jsonb)
|
|| COALESCE(sp.doc -> 'liabilities', '[]'::jsonb)
|
||||||
|| COALESCE(sp.doc -> 'liabilities', '[]'::jsonb)
|
|| COALESCE(sp.doc -> 'expenses', '[]'::jsonb)
|
||||||
|| COALESCE(sp.doc -> 'expenses', '[]'::jsonb)
|
|| 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 lower(trim(m.entry ->> 'accountName')) = lower(trim(?))
|
||||||
WHERE m.entry ->> 'accountName' = ?
|
LIMIT 1
|
||||||
LIMIT 1
|
""", Long.class, savingsProductId, accountName);
|
||||||
""", Long.class, savingsProductId, accountName);
|
if (!found.isEmpty()) {
|
||||||
return found.isEmpty() ? null : glAccountRepository.findById(found.get(0)).orElse(null);
|
return glAccountRepository.findById(found.get(0)).orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+21
-1
@@ -78,7 +78,8 @@ public class DepositAccountTransactionDataValidator {
|
|||||||
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
|
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
|
||||||
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
|
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
|
||||||
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName,
|
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName,
|
||||||
DepositsApiConstants.waivePreClosurePenaltyParamName, DepositsApiConstants.prematureClosureReasonIdParamName));
|
DepositsApiConstants.toGlAccountIdParamName, DepositsApiConstants.waivePreClosurePenaltyParamName,
|
||||||
|
DepositsApiConstants.prematureClosureReasonIdParamName));
|
||||||
|
|
||||||
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
|
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
|
||||||
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
|
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
|
||||||
@@ -247,6 +248,25 @@ public class DepositAccountTransactionDataValidator {
|
|||||||
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
|
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
|
||||||
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
|
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
|
||||||
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
|
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
|
||||||
|
} else if (accountOnClosureType.isTransferToGl()) {
|
||||||
|
// Only the FD premature-closure handler books the GL leg. Offering it anywhere else would
|
||||||
|
// fall through to that handler's terminal "withdraw the whole balance" case and silently pay
|
||||||
|
// the deposit out in cash, which is exactly what the partial.reinvest guard above prevents.
|
||||||
|
if (!isPreMatureClose || !DepositAccountType.FIXED_DEPOSIT.equals(depositAccountType)) {
|
||||||
|
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).failWithCode(
|
||||||
|
"transfer.to.gl.not.allowed",
|
||||||
|
"Transfer to a GL account is only supported for fixed-deposit premature closure");
|
||||||
|
}
|
||||||
|
final Long toGlAccountId = this.fromApiJsonHelper.extractLongNamed(DepositsApiConstants.toGlAccountIdParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(DepositsApiConstants.toGlAccountIdParamName).value(toGlAccountId)
|
||||||
|
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName, accountOnClosureType.getCode());
|
||||||
|
} else if (accountOnClosureType.isWithdarwDeposit() && isPreMatureClose) {
|
||||||
|
// Premature closure proceeds have to be traceable into a savings account, so the cash payout
|
||||||
|
// is not offered in the preview (see DepositAccountPreMatureCalculationPlatformServiceImpl)
|
||||||
|
// and is refused here as well -- otherwise a direct API call could still take the cash route.
|
||||||
|
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).failWithCode(
|
||||||
|
"withdraw.deposit.not.allowed",
|
||||||
|
"Paying the deposit out in cash is not supported for account pre mature close -- transfer it to a savings account");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+96
@@ -25,6 +25,8 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavin
|
|||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferDescriptionParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferDescriptionParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.waivePreClosurePenaltyParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.waivePreClosurePenaltyParamName;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -38,6 +40,7 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.accounting.common.AccountingConstants;
|
||||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||||
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat;
|
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat;
|
||||||
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper;
|
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper;
|
||||||
@@ -48,6 +51,7 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
|||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
||||||
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
||||||
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
||||||
@@ -76,6 +80,7 @@ import org.apache.fineract.portfolio.savings.data.FdInterestRateSlabData;
|
|||||||
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -96,6 +101,8 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
private final FixedDepositRenewalHistoryRepository fixedDepositRenewalHistoryRepository;
|
private final FixedDepositRenewalHistoryRepository fixedDepositRenewalHistoryRepository;
|
||||||
private final FixedDepositRenewalChartSlabHistoryRepository fixedDepositRenewalChartSlabHistoryRepository;
|
private final FixedDepositRenewalChartSlabHistoryRepository fixedDepositRenewalChartSlabHistoryRepository;
|
||||||
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
|
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -846,6 +853,20 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
toSavingsAccount, account, isRegularTransaction, isExceptionForBalanceCheck);
|
toSavingsAccount, account, isRegularTransaction, isExceptionForBalanceCheck);
|
||||||
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
||||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||||
|
} else if (closureType.isTransferToGl()) {
|
||||||
|
final Long toGlAccountId = command.longValueOfParameterNamed(DepositsApiConstants.toGlAccountIdParamName);
|
||||||
|
final BigDecimal proceeds = account.getAccountBalance();
|
||||||
|
// Declared as an account transfer so the withdrawal's own entry credits the Liability Transfer
|
||||||
|
// account rather than cash/vault -- nothing is paid over the counter here. The offset below then
|
||||||
|
// moves it out of there into the GL the maker picked, leaving Liability Transfer at nil and the
|
||||||
|
// net posting as Dr Savings Control / Cr chosen GL.
|
||||||
|
final boolean isInternalTransfer = true;
|
||||||
|
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, proceeds, paymentDetail, false,
|
||||||
|
isRegularTransaction, isInternalTransfer);
|
||||||
|
savingsTransactionId = withdrawal.getId();
|
||||||
|
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||||
|
postClosureProceedsToGlAccount(account, toGlAccountId, proceeds, closedDate,
|
||||||
|
command.stringValueOfParameterNamed(transferDescriptionParamName));
|
||||||
} else {
|
} else {
|
||||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
||||||
paymentDetail, false, isRegularTransaction);
|
paymentDetail, false, isRegularTransaction);
|
||||||
@@ -863,6 +884,81 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
return savingsTransactionId;
|
return savingsTransactionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a premature closure's proceeds out of Liability Transfer and into the GL account the maker picked:
|
||||||
|
* Dr the Liability Transfer account / Cr that GL account.
|
||||||
|
* <p>
|
||||||
|
* The withdrawal booked immediately before this was declared an account transfer, so the accounting engine
|
||||||
|
* has already credited Liability Transfer for the same amount -- this entry clears it, leaving the pair
|
||||||
|
* netting to Dr Savings Control / Cr the chosen GL, and Liability Transfer back at nil. Posted through the real
|
||||||
|
* createJournalEntry command rather than written straight to the ledger, so it reverses, reports and
|
||||||
|
* reconciles exactly like any other manual entry.
|
||||||
|
*/
|
||||||
|
private void postClosureProceedsToGlAccount(final SavingsAccount account, final Long toGlAccountId, final BigDecimal amount,
|
||||||
|
final LocalDate closedDate, final String description) {
|
||||||
|
|
||||||
|
final Long suspenseGlAccountId = liabilityTransferGlAccountId();
|
||||||
|
|
||||||
|
final JsonObject debitEntry = new JsonObject();
|
||||||
|
debitEntry.addProperty("glAccountId", suspenseGlAccountId);
|
||||||
|
debitEntry.addProperty("amount", amount);
|
||||||
|
final JsonArray debits = new JsonArray();
|
||||||
|
debits.add(debitEntry);
|
||||||
|
|
||||||
|
final JsonObject creditEntry = new JsonObject();
|
||||||
|
creditEntry.addProperty("glAccountId", toGlAccountId);
|
||||||
|
creditEntry.addProperty("amount", amount);
|
||||||
|
final JsonArray credits = new JsonArray();
|
||||||
|
credits.add(creditEntry);
|
||||||
|
|
||||||
|
final StringBuilder comments = new StringBuilder("Premature closure of fixed deposit ").append(account.getAccountNumber());
|
||||||
|
if (description != null && !description.isBlank()) {
|
||||||
|
comments.append(" -- ").append(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String dateFormat = "dd MMMM yyyy";
|
||||||
|
final JsonObject payload = new JsonObject();
|
||||||
|
payload.addProperty("officeId", account.officeId());
|
||||||
|
payload.addProperty("transactionDate", closedDate.format(DateTimeFormatter.ofPattern(dateFormat, Locale.ENGLISH)));
|
||||||
|
payload.addProperty("dateFormat", dateFormat);
|
||||||
|
payload.addProperty("locale", "en");
|
||||||
|
payload.addProperty("currencyCode", account.getCurrency().getCode());
|
||||||
|
payload.addProperty("comments", comments.toString());
|
||||||
|
payload.add("debits", debits);
|
||||||
|
payload.add("credits", credits);
|
||||||
|
|
||||||
|
final String json = payload.toString();
|
||||||
|
final JsonCommand journalEntryCommand = JsonCommand.from(json, this.fromApiJsonHelper.parse(json), this.fromApiJsonHelper, null,
|
||||||
|
null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||||
|
this.journalEntryWritePlatformService.createJournalEntry(journalEntryCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GL account the withdrawal above credited, and therefore the exact account this entry has to debit
|
||||||
|
* to clear it.
|
||||||
|
* <p>
|
||||||
|
* It is the organisation-wide Liability Transfer financial activity, NOT the product's own Transfers In
|
||||||
|
* Suspense mapping: {@code CashBasedAccountingProcessorForSavings} books an account-transfer withdrawal
|
||||||
|
* as Dr Savings Control / Cr {@code FinancialActivity.LIABILITY_TRANSFER}. Debiting the product mapping
|
||||||
|
* instead leaves the credit standing in Liability Transfer for ever while the two legs of this entry
|
||||||
|
* collapse onto whichever account that mapping names.
|
||||||
|
* <p>
|
||||||
|
* Refusing outright when it is unmapped is deliberate: silently picking another account would put the
|
||||||
|
* closure's money somewhere nobody chose.
|
||||||
|
*/
|
||||||
|
private Long liabilityTransferGlAccountId() {
|
||||||
|
final List<Long> glAccountIds = this.jdbcTemplate.queryForList(
|
||||||
|
"SELECT gl_account_id FROM acc_gl_financial_activity_account WHERE financial_activity_type = ? ORDER BY id DESC",
|
||||||
|
Long.class, AccountingConstants.FinancialActivity.LIABILITY_TRANSFER.getValue());
|
||||||
|
if (glAccountIds.isEmpty()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.liability.transfer.financial.activity.not.mapped",
|
||||||
|
"No Liability Transfer account is mapped under Accounting > Financial Activity Mappings, so the closure"
|
||||||
|
+ " proceeds cannot be routed to a GL account. Map it first.",
|
||||||
|
AccountingConstants.FinancialActivity.LIABILITY_TRANSFER.getValue());
|
||||||
|
}
|
||||||
|
return glAccountIds.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A staff-triggered, always-available alternative to Premature Close (Active-only, no reinvest) and Close
|
* A staff-triggered, always-available alternative to Premature Close (Active-only, no reinvest) and Close
|
||||||
* (Matured-only): works on an FD in either state, and adds a Partial Reinvest option neither of those two support
|
* (Matured-only): works on an FD in either state, and adds a Partial Reinvest option neither of those two support
|
||||||
|
|||||||
+24
-11
@@ -829,17 +829,22 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,
|
final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||||
final Integer financialYearBeginningMonth) {
|
final Integer financialYearBeginningMonth) {
|
||||||
final MathContext mc = MathContext.DECIMAL64;
|
final MathContext mc = MathContext.DECIMAL64;
|
||||||
final List<PostingPeriod> postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure,
|
this.resetAccountTransactionsEndOfDayBalances(transactions, preMatureDate);
|
||||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
try {
|
||||||
|
final List<PostingPeriod> postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure,
|
||||||
|
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||||
|
|
||||||
Money interestOnMaturity = Money.zero(this.currency);
|
Money interestOnMaturity = Money.zero(this.currency);
|
||||||
|
|
||||||
for (final PostingPeriod interestPostingPeriod : postingPeriods) {
|
for (final PostingPeriod interestPostingPeriod : postingPeriods) {
|
||||||
final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned();
|
final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned();
|
||||||
interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod);
|
interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interestOnMaturity;
|
||||||
|
} finally {
|
||||||
|
this.resetAccountTransactionsEndOfDayBalances(transactions, DateUtils.getBusinessLocalDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
return interestOnMaturity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void postInterest(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer,
|
public void postInterest(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer,
|
||||||
@@ -946,13 +951,21 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
* the term restarted belongs to a term that has already been settled.
|
* the term restarted belongs to a term that has already been settled.
|
||||||
*/
|
*/
|
||||||
private Money totalInterestPosted() {
|
private Money totalInterestPosted() {
|
||||||
|
final boolean renewedTerm = isRenewedTerm();
|
||||||
final LocalDate currentTermStartDate = depositStartDate();
|
final LocalDate currentTermStartDate = depositStartDate();
|
||||||
Money interestPostedToDate = Money.zero(this.currency);
|
Money interestPostedToDate = Money.zero(this.currency);
|
||||||
for (final SavingsAccountTransaction transaction : this.transactions) {
|
for (final SavingsAccountTransaction transaction : this.transactions) {
|
||||||
if (transaction.isInterestPostingAndNotReversed()
|
if (!transaction.isInterestPostingAndNotReversed()) {
|
||||||
&& !DateUtils.isBefore(transaction.getTransactionDate(), currentTermStartDate)) {
|
continue;
|
||||||
interestPostedToDate = interestPostedToDate.plus(transaction.getAmount(currency));
|
|
||||||
}
|
}
|
||||||
|
// Strictly after, and matching transactionsForCurrentTermInterestCalculation() exactly: the
|
||||||
|
// previous term's closing interest posting is dated the SAME day the new term starts, so an
|
||||||
|
// inclusive bound still counts it here while the posting-period walk excludes it -- leaving
|
||||||
|
// the old term's entire interest subtracted from a maturity amount it was never part of.
|
||||||
|
if (renewedTerm && !DateUtils.isAfter(transaction.getTransactionDate(), currentTermStartDate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
interestPostedToDate = interestPostedToDate.plus(transaction.getAmount(currency));
|
||||||
}
|
}
|
||||||
|
|
||||||
return interestPostedToDate;
|
return interestPostedToDate;
|
||||||
|
|||||||
+13
-8
@@ -759,17 +759,22 @@ public class RecurringDepositAccount extends SavingsAccount {
|
|||||||
final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,
|
final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||||
final Integer financialYearBeginningMonth) {
|
final Integer financialYearBeginningMonth) {
|
||||||
final MathContext mc = MathContext.DECIMAL64;
|
final MathContext mc = MathContext.DECIMAL64;
|
||||||
final List<PostingPeriod> postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure,
|
this.resetAccountTransactionsEndOfDayBalances(transactions, preMatureDate);
|
||||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
try {
|
||||||
|
final List<PostingPeriod> postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure,
|
||||||
|
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||||
|
|
||||||
Money interestOnMaturity = Money.zero(this.currency);
|
Money interestOnMaturity = Money.zero(this.currency);
|
||||||
|
|
||||||
for (final PostingPeriod interestPostingPeriod : postingPeriods) {
|
for (final PostingPeriod interestPostingPeriod : postingPeriods) {
|
||||||
final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned();
|
final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned();
|
||||||
interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod);
|
interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod);
|
||||||
|
}
|
||||||
|
this.summary.updateFromInterestPeriodSummaries(this.currency, postingPeriods);
|
||||||
|
return interestOnMaturity;
|
||||||
|
} finally {
|
||||||
|
this.resetAccountTransactionsEndOfDayBalances(transactions, DateUtils.getBusinessLocalDate());
|
||||||
}
|
}
|
||||||
this.summary.updateFromInterestPeriodSummaries(this.currency, postingPeriods);
|
|
||||||
return interestOnMaturity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+11
-3
@@ -70,9 +70,17 @@ public class DepositAccountPreMatureCalculationPlatformServiceImpl implements De
|
|||||||
final SavingsAccount account = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType);
|
final SavingsAccount account = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType);
|
||||||
|
|
||||||
DepositAccountData accountData = null;
|
DepositAccountData accountData = null;
|
||||||
Collection<EnumOptionData> onAccountClosureOptions = SavingsEnumerations
|
// Premature closure proceeds must stay traceable -- into the customer's savings account, or (fixed
|
||||||
.depositAccountOnClosureType(new DepositAccountOnClosureType[] { DepositAccountOnClosureType.WITHDRAW_DEPOSIT,
|
// deposits only) straight to a GL account the maker picks. Paying a broken deposit out over the
|
||||||
DepositAccountOnClosureType.TRANSFER_TO_SAVINGS });
|
// counter in cash is not offered: WITHDRAW_DEPOSIT is deliberately absent here and rejected outright
|
||||||
|
// by DepositAccountTransactionDataValidator#validateClosing, so a direct API call cannot take it
|
||||||
|
// either. TRANSFER_TO_GL is offered only for fixed deposits because only
|
||||||
|
// DepositAccountDomainServiceJpa#handleFDAccountPreMatureClosure books its GL leg.
|
||||||
|
final DepositAccountOnClosureType[] closureTypes = depositAccountType.isFixedDeposit()
|
||||||
|
? new DepositAccountOnClosureType[] { DepositAccountOnClosureType.TRANSFER_TO_SAVINGS,
|
||||||
|
DepositAccountOnClosureType.TRANSFER_TO_GL }
|
||||||
|
: new DepositAccountOnClosureType[] { DepositAccountOnClosureType.TRANSFER_TO_SAVINGS };
|
||||||
|
Collection<EnumOptionData> onAccountClosureOptions = SavingsEnumerations.depositAccountOnClosureType(closureTypes);
|
||||||
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
||||||
final Collection<SavingsAccountData> savingsAccountDatas = this.savingsAccountReadPlatformService
|
final Collection<SavingsAccountData> savingsAccountDatas = this.savingsAccountReadPlatformService
|
||||||
.retrieveActiveForLookup(account.clientId(), DepositAccountType.SAVINGS_DEPOSIT);
|
.retrieveActiveForLookup(account.clientId(), DepositAccountType.SAVINGS_DEPOSIT);
|
||||||
|
|||||||
+5
-1
@@ -543,9 +543,13 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
|||||||
|
|
||||||
DepositAccountData account = this.jdbcTemplate.queryForObject(sqlBuilder.toString(), depositAccountMapper, accountId,
|
DepositAccountData account = this.jdbcTemplate.queryForObject(sqlBuilder.toString(), depositAccountMapper, accountId,
|
||||||
depositAccountType.getValue());
|
depositAccountType.getValue());
|
||||||
|
// TRANSFER_TO_GL is excluded for the same reason PARTIAL_REINVEST is filtered above: only one
|
||||||
|
// handler implements it (fixed-deposit PREMATURE closure, which builds its own option list in
|
||||||
|
// DepositAccountPreMatureCalculationPlatformServiceImpl). Offering it on this template -- the
|
||||||
|
// normal Close and Payment screens -- would only get it refused by validateClosing.
|
||||||
Collection<EnumOptionData> onAccountClosureOptions = SavingsEnumerations
|
Collection<EnumOptionData> onAccountClosureOptions = SavingsEnumerations
|
||||||
.depositAccountOnClosureType(Arrays.stream(DepositAccountOnClosureType.values())
|
.depositAccountOnClosureType(Arrays.stream(DepositAccountOnClosureType.values())
|
||||||
.filter(type -> includePartialReinvest || !type.isPartialReinvest())
|
.filter(type -> includePartialReinvest || !type.isPartialReinvest()).filter(type -> !type.isTransferToGl())
|
||||||
.toArray(DepositAccountOnClosureType[]::new));
|
.toArray(DepositAccountOnClosureType[]::new));
|
||||||
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
||||||
final Collection<SavingsAccountData> savingsAccountDatas = this.savingsAccountReadPlatformService
|
final Collection<SavingsAccountData> savingsAccountDatas = this.savingsAccountReadPlatformService
|
||||||
|
|||||||
+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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-3
@@ -437,14 +437,38 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Collection<Long> interestTransferWithdrawalTransactionIds(final SavingsAccountData savingsAccountData) {
|
||||||
|
if (savingsAccountData.getId() == null || this.savingsHelper == null || !isTransferInterestToOtherAccount(savingsAccountData)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return this.savingsHelper.fetchPostInterestTransactionIds(savingsAccountData.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTransferInterestToOtherAccount(final SavingsAccountData savingsAccountData) {
|
||||||
|
return savingsAccountData.isTransferInterestToLinkedAccount()
|
||||||
|
&& DepositAccountType.FIXED_DEPOSIT.getValue().equals(savingsAccountData.getDepositTypeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<SavingsAccountTransactionData> excludeInterestTransferWithdrawals(
|
||||||
|
final List<SavingsAccountTransactionData> transactions, final SavingsAccountData savingsAccountData) {
|
||||||
|
final Collection<Long> interestTransferWithdrawalIds = interestTransferWithdrawalTransactionIds(savingsAccountData);
|
||||||
|
if (interestTransferWithdrawalIds.isEmpty()) {
|
||||||
|
return transactions;
|
||||||
|
}
|
||||||
|
return transactions.stream().filter(transaction -> !interestTransferWithdrawalIds.contains(transaction.getId()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
private List<SavingsAccountTransactionData> retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData savingsAccountData) {
|
private List<SavingsAccountTransactionData> retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData savingsAccountData) {
|
||||||
final List<SavingsAccountTransactionData> listOfTransactionsSorted = retrieveListOfTransactions(savingsAccountData);
|
final List<SavingsAccountTransactionData> listOfTransactionsSorted = retrieveListOfTransactions(savingsAccountData);
|
||||||
|
final Collection<Long> interestTransferWithdrawalIds = interestTransferWithdrawalTransactionIds(savingsAccountData);
|
||||||
|
|
||||||
final List<SavingsAccountTransactionData> orderedNonInterestPostingTransactions = new ArrayList<>();
|
final List<SavingsAccountTransactionData> orderedNonInterestPostingTransactions = new ArrayList<>();
|
||||||
|
|
||||||
for (final SavingsAccountTransactionData transaction : listOfTransactionsSorted) {
|
for (final SavingsAccountTransactionData transaction : listOfTransactionsSorted) {
|
||||||
if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())
|
if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())
|
||||||
&& transaction.isNotReversed() && !transaction.isReversalTransaction()) {
|
&& transaction.isNotReversed() && !transaction.isReversalTransaction()
|
||||||
|
&& !interestTransferWithdrawalIds.contains(transaction.getId())) {
|
||||||
orderedNonInterestPostingTransactions.add(transaction);
|
orderedNonInterestPostingTransactions.add(transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -664,10 +688,12 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
|
|||||||
|
|
||||||
protected void resetAccountTransactionsEndOfDayBalances(final List<SavingsAccountTransactionData> accountTransactionsSorted,
|
protected void resetAccountTransactionsEndOfDayBalances(final List<SavingsAccountTransactionData> accountTransactionsSorted,
|
||||||
final LocalDate interestPostingUpToDate, final SavingsAccountData savingsAccountData) {
|
final LocalDate interestPostingUpToDate, final SavingsAccountData savingsAccountData) {
|
||||||
|
final List<SavingsAccountTransactionData> filteredTransactions = excludeInterestTransferWithdrawals(accountTransactionsSorted,
|
||||||
|
savingsAccountData);
|
||||||
// loop over transactions in reverse
|
// loop over transactions in reverse
|
||||||
LocalDate endOfBalanceDate = interestPostingUpToDate;
|
LocalDate endOfBalanceDate = interestPostingUpToDate;
|
||||||
for (int i = accountTransactionsSorted.size() - 1; i >= 0; i--) {
|
for (int i = filteredTransactions.size() - 1; i >= 0; i--) {
|
||||||
final SavingsAccountTransactionData transaction = accountTransactionsSorted.get(i);
|
final SavingsAccountTransactionData transaction = filteredTransactions.get(i);
|
||||||
if (transaction.isNotReversed() && !transaction.isReversalTransaction()
|
if (transaction.isNotReversed() && !transaction.isReversalTransaction()
|
||||||
&& !(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())) {
|
&& !(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())) {
|
||||||
transaction.updateCumulativeBalanceAndDates(MonetaryCurrency.fromCurrencyData(savingsAccountData.getCurrency()),
|
transaction.updateCumulativeBalanceAndDates(MonetaryCurrency.fromCurrencyData(savingsAccountData.getCurrency()),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat
|
|||||||
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
||||||
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
||||||
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
||||||
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:Keshav@898966}
|
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:313863}
|
||||||
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
||||||
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
||||||
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
||||||
@@ -415,7 +415,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
|
|||||||
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
|
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
|
||||||
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
||||||
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
||||||
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:Keshav@898966}
|
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
|
||||||
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
||||||
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
||||||
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
||||||
|
|||||||
+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