Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c49d6f637 | ||
|
|
331c553164 | ||
|
|
3fb008a22a | ||
|
|
8b2fb052d4 |
+16
-7
@@ -651,6 +651,8 @@ public class AccountingProcessorHelper {
|
|||||||
|
|
||||||
final GLAccount controlAccount = getLinkedGLAccountForSavingsProduct(savingsProductId,
|
final GLAccount controlAccount = getLinkedGLAccountForSavingsProduct(savingsProductId,
|
||||||
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), paymentTypeId);
|
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), paymentTypeId);
|
||||||
|
final GLAccount interestOnSavingsAccount = getLinkedGLAccountForSavingsProduct(savingsProductId,
|
||||||
|
CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), paymentTypeId);
|
||||||
|
|
||||||
// The payable comes from the Interest Payable row on the product's Accounting tab, which is
|
// The payable comes from the Interest Payable row on the product's Accounting tab, which is
|
||||||
// where a bank actually picks it. The reserved name in the product's other GL mappings is
|
// where a bank actually picks it. The reserved name in the product's other GL mappings is
|
||||||
@@ -664,13 +666,20 @@ public class AccountingProcessorHelper {
|
|||||||
final GLAccount paidAccount = Optional.ofNullable(namedSavingsGlAccount(savingsProductId, SAVINGS_INTEREST_PAID_MAPPING))
|
final GLAccount paidAccount = Optional.ofNullable(namedSavingsGlAccount(savingsProductId, SAVINGS_INTEREST_PAID_MAPPING))
|
||||||
.orElse(payableAccount);
|
.orElse(payableAccount);
|
||||||
|
|
||||||
// The interest expense is not raised here. It is recognised as the interest accrues, day by
|
// Stage 1: the expense is recognised against whatever the liability sits in right now -- the
|
||||||
// day, and this posting settles that recognised debt - raising the expense again at payout
|
// named Payable account if the product has one, or straight against Savings Control if it
|
||||||
// would put the same interest on the P&L twice. This mirrors the pooled posting service,
|
// doesn't. This was missing entirely: every product, configured or not, posted zero journal
|
||||||
// which pays a product's whole membership the same way; a closure reaching this path must
|
// entries for interest until this leg existed, since a product with no Payable/Paid names
|
||||||
// produce the same entries as the nightly run, or the two disagree on what one payment did.
|
// configured also collapsed stages 2 and 3 below into a same-account no-op with nothing else
|
||||||
|
// to post in their place.
|
||||||
|
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, payableAccount, savingsId, transactionId, transactionDate, amount,
|
||||||
|
isReversal);
|
||||||
|
|
||||||
|
// The interest expense is not raised again here. It was just raised above, and this posting
|
||||||
|
// only settles that recognised debt onward to where it is actually paid out from -- raising
|
||||||
|
// the expense a second time would put the same interest on the P&L twice.
|
||||||
//
|
//
|
||||||
// Stage 1: the liability moves from the payable to the account it is paid out of. Where the
|
// Stage 2: the liability moves from the payable to the account it is paid out of. Where the
|
||||||
// product names no paid account both resolve to the payable and this says nothing, so it is
|
// product names no paid account both resolve to the payable and this says nothing, so it is
|
||||||
// skipped and the payout comes straight out of the payable.
|
// skipped and the payout comes straight out of the payable.
|
||||||
final boolean payableIsPaid = payableAccount.getId().equals(paidAccount.getId());
|
final boolean payableIsPaid = payableAccount.getId().equals(paidAccount.getId());
|
||||||
@@ -679,7 +688,7 @@ public class AccountingProcessorHelper {
|
|||||||
isReversal);
|
isReversal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 2: into the member's own balance.
|
// Stage 3: into the member's own balance.
|
||||||
final GLAccount payoutFromAccount = payableIsPaid ? payableAccount : paidAccount;
|
final GLAccount payoutFromAccount = payableIsPaid ? payableAccount : paidAccount;
|
||||||
postSavingsLeg(office, currencyCode, payoutFromAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
postSavingsLeg(office, currencyCode, payoutFromAccount, controlAccount, savingsId, transactionId, transactionDate, amount,
|
||||||
isReversal);
|
isReversal);
|
||||||
|
|||||||
+10
-4
@@ -33,8 +33,12 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FD Dashboard's data source: every ACTIVE Fixed Deposit with just the fields the maturity ladder
|
* FD Dashboard's data source: every Fixed Deposit still sitting on its own account -- ACTIVE, or the
|
||||||
* needs (account/client/product identity, maturity date, deposit and maturity amounts).
|
* real MATURED(800) status the nightly maturity job sets once it's past maturity_date with no
|
||||||
|
* closure instruction to auto-process -- with just the fields the maturity ladder needs
|
||||||
|
* (account/client/product identity, maturity date, deposit and maturity amounts). A MATURED FD stays
|
||||||
|
* here until it's actually renewed or closed, since that's exactly the "needs manual attention"
|
||||||
|
* state the ladder exists to surface.
|
||||||
*
|
*
|
||||||
* Deliberately not routed through {@link FixedDepositAccountsApiResource#retrieveAll}: that
|
* Deliberately not routed through {@link FixedDepositAccountsApiResource#retrieveAll}: that
|
||||||
* endpoint's mapper joins m_savings_account to m_client, m_group, m_staff, m_currency,
|
* endpoint's mapper joins m_savings_account to m_client, m_group, m_staff, m_currency,
|
||||||
@@ -57,6 +61,8 @@ public class FixedDepositMaturityDashboardApiResource {
|
|||||||
private static final int FIXED_DEPOSIT = 200;
|
private static final int FIXED_DEPOSIT = 200;
|
||||||
/** SavingsAccountStatusType.ACTIVE. */
|
/** SavingsAccountStatusType.ACTIVE. */
|
||||||
private static final int ACTIVE = 300;
|
private static final int ACTIVE = 300;
|
||||||
|
/** SavingsAccountStatusType.MATURED -- set by the nightly job once past maturity_date. */
|
||||||
|
private static final int MATURED = 800;
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
@@ -87,9 +93,9 @@ public class FixedDepositMaturityDashboardApiResource {
|
|||||||
JOIN m_savings_product sp ON sp.id = sa.product_id
|
JOIN m_savings_product sp ON sp.id = sa.product_id
|
||||||
LEFT JOIN m_deposit_product_term_and_preclosure dptp ON dptp.savings_product_id = sp.id
|
LEFT JOIN m_deposit_product_term_and_preclosure dptp ON dptp.savings_product_id = sp.id
|
||||||
LEFT JOIN m_client c ON c.id = sa.client_id
|
LEFT JOIN m_client c ON c.id = sa.client_id
|
||||||
WHERE sa.deposit_type_enum = ? AND sa.status_enum = ?
|
WHERE sa.deposit_type_enum = ? AND sa.status_enum IN (?, ?)
|
||||||
ORDER BY datp.maturity_date ASC
|
ORDER BY datp.maturity_date ASC
|
||||||
""", FIXED_DEPOSIT, ACTIVE);
|
""", FIXED_DEPOSIT, ACTIVE, MATURED);
|
||||||
|
|
||||||
return this.gson.toJson(rows);
|
return this.gson.toJson(rows);
|
||||||
}
|
}
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.savings.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only: what a Renew from the FD Dashboard is about to do, before the staff member confirms
|
||||||
|
* anything. The Payment dialog calls this on open (dashboardRenewal mode) to show the current
|
||||||
|
* balance, the linked account's rate, the gap-day count, the computed gap-period interest, and the
|
||||||
|
* balance the new term will actually start from -- exactly the numbers
|
||||||
|
* {@code creditGapInterestIfPastGrace} would post, just not posted yet. Nothing here mutates the
|
||||||
|
* account, so this only needs read permission, unlike its posting sibling.
|
||||||
|
*/
|
||||||
|
@Path("/v1/fixeddepositaccounts/{accountId}/renewal-preview")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Fixed Deposit Accounts", description = "FD Dashboard: read-only preview of a Renew action's gap-interest math.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FixedDepositRenewalPreviewApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final DepositAccountWritePlatformService depositAccountWritePlatformService;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String preview(@PathParam("accountId") final Long accountId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME);
|
||||||
|
final Map<String, Object> result = this.depositAccountWritePlatformService.previewFdRenewal(accountId);
|
||||||
|
return this.gson.toJson(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
+109
-31
@@ -1554,7 +1554,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
// an account opened straight into Reinvest Principal Only can reach here with none set.
|
// an account opened straight into Reinvest Principal Only can reach here with none set.
|
||||||
// Falls back to the FD's own Deposit Account Link rather than failing the whole job run.
|
// Falls back to the FD's own Deposit Account Link rather than failing the whole job run.
|
||||||
if (toSavingsId == null && fdAccount.isReinvestOnClosure()) {
|
if (toSavingsId == null && fdAccount.isReinvestOnClosure()) {
|
||||||
toSavingsId = fdAccount.getLinkedAccountId();
|
toSavingsId = resolveLinkedAccountId(fdAccount.getId());
|
||||||
}
|
}
|
||||||
this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(),
|
this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(),
|
||||||
fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes);
|
fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes);
|
||||||
@@ -1620,7 +1620,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
// Only Principal pays the leftover interest out to the FD's own linked Savings account -- the
|
// Only Principal pays the leftover interest out to the FD's own linked Savings account -- the
|
||||||
// account-opening wizard makes a Deposit Account Link mandatory for SINGLE/DOUBLE FDs, so this
|
// account-opening wizard makes a Deposit Account Link mandatory for SINGLE/DOUBLE FDs, so this
|
||||||
// should always be present, but fail clearly rather than silently stranding the interest if not.
|
// should always be present, but fail clearly rather than silently stranding the interest if not.
|
||||||
final Long linkedSavingsAccountId = fdAccount.getLinkedAccountId();
|
final Long linkedSavingsAccountId = resolveLinkedAccountId(fdAccount.getId());
|
||||||
if (chosenClosureType.isReinvestPrincipal() && linkedSavingsAccountId == null) {
|
if (chosenClosureType.isReinvestPrincipal() && linkedSavingsAccountId == null) {
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.renewal.linked.account.required",
|
throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.renewal.linked.account.required",
|
||||||
"This FD has no Deposit Account Link -- link a Savings account to it before renewing with Principal Only, "
|
"This FD has no Deposit Account Link -- link a Savings account to it before renewing with Principal Only, "
|
||||||
@@ -1630,13 +1630,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
// Renewal grace period only shrinks the backdate window below -- it is no longer a precondition
|
// Renewal grace period only shrinks the backdate window below -- it is no longer a precondition
|
||||||
// for whether Renew is allowed at all. A product that never configured one behaves as a 0-day
|
// for whether Renew is allowed at all. A product that never configured one behaves as a 0-day
|
||||||
// window: renewing on the maturity date itself backdates, any later renewal pays gap interest.
|
// window: renewing on the maturity date itself backdates, any later renewal pays gap interest.
|
||||||
Integer renewalGracePeriodDays = null;
|
final int renewalGracePeriodDays = resolveRenewalGracePeriodDays(fdAccount);
|
||||||
if (fdAccount.savingsProduct() instanceof FixedDepositProduct fdProduct && fdProduct.depositProductTermAndPreClosure() != null) {
|
|
||||||
renewalGracePeriodDays = fdProduct.depositProductTermAndPreClosure().depositTermDetail().renewalGracePeriodDays();
|
|
||||||
}
|
|
||||||
if (renewalGracePeriodDays == null || renewalGracePeriodDays < 0) {
|
|
||||||
renewalGracePeriodDays = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
final LocalDate maturityDate = fdAccount.maturityDate();
|
final LocalDate maturityDate = fdAccount.maturityDate();
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
@@ -1697,17 +1691,9 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
"This FD has not matured yet.");
|
"This FD has not matured yet.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer renewalGracePeriodDays = null;
|
|
||||||
if (fdAccount.savingsProduct() instanceof FixedDepositProduct fdProduct && fdProduct.depositProductTermAndPreClosure() != null) {
|
|
||||||
renewalGracePeriodDays = fdProduct.depositProductTermAndPreClosure().depositTermDetail().renewalGracePeriodDays();
|
|
||||||
}
|
|
||||||
if (renewalGracePeriodDays == null || renewalGracePeriodDays < 0) {
|
|
||||||
renewalGracePeriodDays = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
final LocalDate maturityDate = fdAccount.maturityDate();
|
final LocalDate maturityDate = fdAccount.maturityDate();
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
final boolean withinGrace = !today.isAfter(maturityDate.plusDays(renewalGracePeriodDays));
|
final boolean withinGrace = !today.isAfter(maturityDate.plusDays(resolveRenewalGracePeriodDays(fdAccount)));
|
||||||
|
|
||||||
final Map<String, Object> result = new HashMap<>();
|
final Map<String, Object> result = new HashMap<>();
|
||||||
result.put("withinGrace", withinGrace);
|
result.put("withinGrace", withinGrace);
|
||||||
@@ -1715,6 +1701,108 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only preview of what a Renew from the FD Dashboard is about to do, shown in the Payment
|
||||||
|
* dialog before the staff member confirms anything -- the actual current balance, the linked
|
||||||
|
* account's rate, the gap-day count, the computed gap-interest amount, and the balance the FD will
|
||||||
|
* carry into its new term once that interest lands. Mirrors {@link #creditGapInterest} exactly but
|
||||||
|
* never posts anything; a within-grace account gets zero for the gap fields since nothing is owed.
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> previewFdRenewal(final Long fdAccountId) {
|
||||||
|
final FixedDepositAccount fdAccount = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(fdAccountId,
|
||||||
|
DepositAccountType.FIXED_DEPOSIT);
|
||||||
|
final Map<String, Object> result = new HashMap<>();
|
||||||
|
if (!fdAccount.isMatured()) {
|
||||||
|
result.put("isMatured", false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.put("isMatured", true);
|
||||||
|
|
||||||
|
final int renewalGracePeriodDays = resolveRenewalGracePeriodDays(fdAccount);
|
||||||
|
final LocalDate maturityDate = fdAccount.maturityDate();
|
||||||
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
|
final boolean withinGrace = !today.isAfter(maturityDate.plusDays(renewalGracePeriodDays));
|
||||||
|
final BigDecimal currentBalance = fdAccount.getAccountBalance();
|
||||||
|
|
||||||
|
result.put("maturityDate", maturityDate.toString());
|
||||||
|
result.put("renewalGracePeriodDays", renewalGracePeriodDays);
|
||||||
|
result.put("withinGrace", withinGrace);
|
||||||
|
result.put("currentBalance", currentBalance);
|
||||||
|
|
||||||
|
if (withinGrace) {
|
||||||
|
result.put("gapDays", 0L);
|
||||||
|
result.put("gapInterestAmount", BigDecimal.ZERO);
|
||||||
|
result.put("projectedBalance", currentBalance);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final long gapDays = ChronoUnit.DAYS.between(maturityDate, today);
|
||||||
|
result.put("gapDays", gapDays);
|
||||||
|
|
||||||
|
final Long linkedAccountId = resolveLinkedAccountId(fdAccount.getId());
|
||||||
|
result.put("linkedAccountId", linkedAccountId);
|
||||||
|
if (linkedAccountId == null) {
|
||||||
|
// No target to read a rate from -- the caller (the Payment dialog) shows this as a blocking
|
||||||
|
// "link a Savings account first" message rather than a number, same as the posting path does.
|
||||||
|
result.put("gapInterestAmount", null);
|
||||||
|
result.put("projectedBalance", null);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal linkedAccountRate = resolveLinkedAccountRate(linkedAccountId);
|
||||||
|
final BigDecimal gapInterestAmount = computeGapInterestAmount(fdAccount, linkedAccountRate, gapDays);
|
||||||
|
result.put("linkedAccountRate", linkedAccountRate);
|
||||||
|
result.put("gapInterestAmount", gapInterestAmount);
|
||||||
|
result.put("projectedBalance", currentBalance.add(gapInterestAmount));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Null/negative reads as "no grace configured" -- a 0-day window, not a block on renewing. */
|
||||||
|
private int resolveRenewalGracePeriodDays(final FixedDepositAccount fdAccount) {
|
||||||
|
Integer renewalGracePeriodDays = null;
|
||||||
|
if (fdAccount.savingsProduct() instanceof FixedDepositProduct fdProduct && fdProduct.depositProductTermAndPreClosure() != null) {
|
||||||
|
renewalGracePeriodDays = fdProduct.depositProductTermAndPreClosure().depositTermDetail().renewalGracePeriodDays();
|
||||||
|
}
|
||||||
|
return (renewalGracePeriodDays == null || renewalGracePeriodDays < 0) ? 0 : renewalGracePeriodDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The FD's own Deposit Account Link, read fresh via SQL rather than the JPA entity's
|
||||||
|
* {@code getLinkedAccountId()} -- an account whose link was set at creation through the real
|
||||||
|
* account-opening command reads back correctly there, but this call site kept returning null for
|
||||||
|
* an account confirmed to have {@code linked_account_id} set in the database, which every one of
|
||||||
|
* this method's own gap-interest/reinvest-target callers depends on. Reading it directly sidesteps
|
||||||
|
* whatever staleness that entity accessor has, rather than trying to chase it down across every
|
||||||
|
* caller that touches this account within the same request.
|
||||||
|
*/
|
||||||
|
private Long resolveLinkedAccountId(final Long savingsAccountId) {
|
||||||
|
return this.jdbcTemplate.queryForObject("SELECT linked_account_id FROM m_savings_account WHERE id = ?", Long.class,
|
||||||
|
savingsAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal resolveLinkedAccountRate(final Long linkedAccountId) {
|
||||||
|
return this.jdbcTemplate.queryForObject("""
|
||||||
|
SELECT sp.nominal_annual_interest_rate FROM m_savings_account sa
|
||||||
|
JOIN m_savings_product sp ON sp.id = sa.product_id
|
||||||
|
WHERE sa.id = ?
|
||||||
|
""", BigDecimal.class, linkedAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Simple interest on the FD's own current balance, for the stretch between its maturity date and
|
||||||
|
* today, at the linked account's product rate -- the pure calculation, shared by the posting path
|
||||||
|
* ({@link #creditGapInterest}) and the read-only preview ({@link #previewFdRenewal}) so the number
|
||||||
|
* shown to staff before they confirm is exactly the number that gets credited. */
|
||||||
|
private BigDecimal computeGapInterestAmount(final FixedDepositAccount fdAccount, final BigDecimal linkedAccountRate,
|
||||||
|
final long gapDays) {
|
||||||
|
final int daysInYear = fdAccount.getInterestCalculationDaysInYearType() == null ? 365
|
||||||
|
: fdAccount.getInterestCalculationDaysInYearType();
|
||||||
|
final MathContext mc = MoneyHelper.getMathContext();
|
||||||
|
return fdAccount.getAccountBalance().multiply(linkedAccountRate, mc).multiply(BigDecimal.valueOf(gapDays), mc)
|
||||||
|
.divide(BigDecimal.valueOf((long) daysInYear * 100), mc).setScale(2, java.math.RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple interest for the stretch between the FD's own maturity date and today, at the rate of its
|
* Simple interest for the stretch between the FD's own maturity date and today, at the rate of its
|
||||||
* linked Savings/Current account's product -- credited onto the FD itself as a genuine
|
* linked Savings/Current account's product -- credited onto the FD itself as a genuine
|
||||||
@@ -1728,25 +1816,15 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
* two this method's sibling endpoint originally supported.
|
* two this method's sibling endpoint originally supported.
|
||||||
*/
|
*/
|
||||||
private BigDecimal creditGapInterest(final FixedDepositAccount fdAccount, final LocalDate maturityDate, final LocalDate today) {
|
private BigDecimal creditGapInterest(final FixedDepositAccount fdAccount, final LocalDate maturityDate, final LocalDate today) {
|
||||||
final Long linkedAccountId = fdAccount.getLinkedAccountId();
|
final Long linkedAccountId = resolveLinkedAccountId(fdAccount.getId());
|
||||||
if (linkedAccountId == null) {
|
if (linkedAccountId == null) {
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.renewal.linked.account.required",
|
throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.renewal.linked.account.required",
|
||||||
"This FD's grace period has passed and it has no Deposit Account Link -- link a Savings/Current account to it "
|
"This FD's grace period has passed and it has no Deposit Account Link -- link a Savings/Current account to it "
|
||||||
+ "before renewing late, so the gap-period interest has a rate to use.");
|
+ "before renewing late, so the gap-period interest has a rate to use.");
|
||||||
}
|
}
|
||||||
final BigDecimal linkedAccountRate = this.jdbcTemplate.queryForObject("""
|
final BigDecimal linkedAccountRate = resolveLinkedAccountRate(linkedAccountId);
|
||||||
SELECT sp.nominal_annual_interest_rate FROM m_savings_account sa
|
|
||||||
JOIN m_savings_product sp ON sp.id = sa.product_id
|
|
||||||
WHERE sa.id = ?
|
|
||||||
""", BigDecimal.class, linkedAccountId);
|
|
||||||
|
|
||||||
final long gapDays = ChronoUnit.DAYS.between(maturityDate, today);
|
final long gapDays = ChronoUnit.DAYS.between(maturityDate, today);
|
||||||
final int daysInYear = fdAccount.getInterestCalculationDaysInYearType() == null ? 365
|
final BigDecimal gapInterestAmount = computeGapInterestAmount(fdAccount, linkedAccountRate, gapDays);
|
||||||
: fdAccount.getInterestCalculationDaysInYearType();
|
|
||||||
final MathContext mc = MoneyHelper.getMathContext();
|
|
||||||
final BigDecimal gapInterestAmount = fdAccount.getAccountBalance().multiply(linkedAccountRate, mc)
|
|
||||||
.multiply(BigDecimal.valueOf(gapDays), mc).divide(BigDecimal.valueOf((long) daysInYear * 100), mc)
|
|
||||||
.setScale(2, java.math.RoundingMode.HALF_UP);
|
|
||||||
|
|
||||||
if (gapInterestAmount.compareTo(BigDecimal.ZERO) > 0) {
|
if (gapInterestAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||||
|
|||||||
+5
@@ -402,8 +402,13 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
|
|||||||
msg = "Savings product with short name `" + shortName + "` already exists";
|
msg = "Savings product with short name `" + shortName + "` already exists";
|
||||||
param = "shortName";
|
param = "shortName";
|
||||||
msgArgs = new Object[] { shortName, dae };
|
msgArgs = new Object[] { shortName, dae };
|
||||||
|
} else if (checkEx.getMessage().contains("numeric field overflow")) {
|
||||||
|
msgCode += ".amount.too.large";
|
||||||
|
msg = "One of the amount fields entered is too large (Minimum/Maximum Deposit Amount or Pre-Closure Penal Interest). Please enter a realistic value and try again.";
|
||||||
|
msgArgs = new Object[] { dae };
|
||||||
} else {
|
} else {
|
||||||
msgCode += ".unknown.data.integrity.issue";
|
msgCode += ".unknown.data.integrity.issue";
|
||||||
|
msg = "Unknown data integrity issue with savings product. Details: " + checkEx.getMessage();
|
||||||
msgArgs = new Object[] { dae };
|
msgArgs = new Object[] { dae };
|
||||||
}
|
}
|
||||||
log.error("Error occured.", dae);
|
log.error("Error occured.", dae);
|
||||||
|
|||||||
+9
@@ -114,5 +114,14 @@ public interface DepositAccountWritePlatformService {
|
|||||||
*/
|
*/
|
||||||
Map<String, Object> creditGapInterestIfPastGrace(Long fdAccountId);
|
Map<String, Object> creditGapInterestIfPastGrace(Long fdAccountId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only preview of what renewing this matured FD right now would do -- shown in the FD
|
||||||
|
* Dashboard's Payment dialog before staff confirm anything, never posts anything itself. Returns
|
||||||
|
* isMatured (false short-circuits the rest), maturityDate, renewalGracePeriodDays, withinGrace,
|
||||||
|
* currentBalance, gapDays, and -- only once past grace -- linkedAccountId, linkedAccountRate,
|
||||||
|
* gapInterestAmount and projectedBalance (the balance the new term would start from).
|
||||||
|
*/
|
||||||
|
Map<String, Object> previewFdRenewal(Long fdAccountId);
|
||||||
|
|
||||||
SavingsAccountTransaction mandatorySavingsAccountDeposit(SavingsAccountTransactionDTO accountTransactionDTO);
|
SavingsAccountTransaction mandatorySavingsAccountDeposit(SavingsAccountTransactionDTO accountTransactionDTO);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user