feat(fixed-deposits): show the period each posting covers, surface maturity postings, guard against double-posting

Adds interest_period_from to the report line so each posting shows the
span it covers (e.g. "01 Oct - 31 Dec, Quarterly"), not just the date
it ends on, and a postingPeriodLabel on the report itself.

Adds a reconciliation endpoint (outside-approval) that lists fixed
deposit interest posted without this report's approval — chiefly a
deposit that matured before its own posting cycle completed, whose
last period the maturity job settles directly. Read-only, so a branch
can see every rupee of deposit interest it has paid, approved or not.

Guards posting itself against the same situation: before crediting a
line, checks whether its deposit already carries an interest posting
for that exact date (raised by the maturity job in the window between
this report being generated and being posted). If so, the line is
linked to what actually paid it instead of being paid again, and its
amount is reconciled off the report's approved total rather than
failing the whole posting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 00:06:14 +05:30
co-authored by Claude Sonnet 5
parent 949f45c4cd
commit 6fbc3a85a8
4 changed files with 229 additions and 30 deletions
@@ -82,6 +82,18 @@ public class FixedDepositInterestPostingReportService {
private static final int MONEY_SCALE = 2;
private static final RoundingMode MONEY_ROUNDING = RoundingMode.HALF_EVEN;
/**
* The product's posting cycle in words, read off {@code SavingsPostingInterestPeriodType}'s own
* values (1 daily, 4 monthly, 5 quarterly, 6 biannual, 7 annual) so a checker reading a period range
* also reads what kind of period it is, not just its two dates.
*/
private static final String POSTING_PERIOD_LABEL_CASE = """
CASE p.interest_posting_period_enum
WHEN 1 THEN 'Daily' WHEN 4 THEN 'Monthly' WHEN 5 THEN 'Quarterly'
WHEN 6 THEN 'Half-yearly' WHEN 7 THEN 'Yearly' ELSE 'Interest'
END
""";
private final JdbcTemplate jdbcTemplate;
private final SavingsAccountReadPlatformService savingsAccountReadPlatformService;
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;
@@ -120,7 +132,7 @@ public class FixedDepositInterestPostingReportService {
*/
public record DueLine(Long accountId, String accountNo, String clientName, Long officeId, Long productId, String fdType,
LocalDate postingDate, BigDecimal interest, BigDecimal balanceBefore, BigDecimal depositAmount, BigDecimal interestRate,
LocalDate maturityDate, Long payoutSavingsAccountId, String payoutAccountNo) {}
LocalDate maturityDate, Long payoutSavingsAccountId, String payoutAccountNo, LocalDate periodFrom) {}
/** One unpaid posting the engine worked out: the date it belongs to and what it comes to. */
private record ComputedPosting(Long accountId, LocalDate postingDate, BigDecimal interest) {}
@@ -219,7 +231,7 @@ public class FixedDepositInterestPostingReportService {
final String ids = postings.stream().map(ComputedPosting::accountId).distinct().map(String::valueOf)
.collect(Collectors.joining(","));
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList("""
SELECT sa.id AS account_id, sa.account_no, sa.product_id,
SELECT sa.id AS account_id, sa.account_no, sa.product_id, sa.activatedon_date,
COALESCE(c.display_name, g.display_name) AS client_name,
COALESCE(c.office_id, g.office_id) AS office_id,
COALESCE(sa.account_balance_derived, 0) AS balance,
@@ -242,8 +254,19 @@ public class FixedDepositInterestPostingReportService {
byAccount.put(((Number) row.get("account_id")).longValue(), row);
}
// Where each deposit's last paid period ended, so the new one's own period can be shown
// starting the day after — not computed from the posting itself, which only carries the day
// the period ends. A deposit with no interest posted yet starts counting from its activation.
final Map<Long, LocalDate> lastPaidThrough = previousPeriodEnds(byAccount.keySet(), postings);
// Earliest date first within each deposit, so a deposit owed two periods on this report has
// its second period's "from" follow the first's own "to" rather than both reading the same
// opening date.
final List<ComputedPosting> ordered = postings.stream()
.sorted(Comparator.comparing(ComputedPosting::accountId).thenComparing(ComputedPosting::postingDate)).toList();
final List<DueLine> lines = new ArrayList<>();
for (final ComputedPosting posting : postings) {
for (final ComputedPosting posting : ordered) {
final Map<String, Object> row = byAccount.get(posting.accountId());
if (row == null) {
continue;
@@ -259,16 +282,49 @@ public class FixedDepositInterestPostingReportService {
// names an account to pay into.
final boolean paysOut = fdType.isPaidOutMonthly() && Boolean.TRUE.equals(row.get("transfer_interest_to_linked_account"));
final Object payoutId = paysOut ? row.get("transfer_to_savings_account_id") : null;
final LocalDate periodStart = lastPaidThrough.get(posting.accountId());
final LocalDate periodFrom = periodStart == null ? toLocalDate(row.get("activatedon_date")) : periodStart.plusDays(1);
lastPaidThrough.put(posting.accountId(), posting.postingDate());
lines.add(new DueLine(posting.accountId(), (String) row.get("account_no"), (String) row.get("client_name"),
((Number) officeId).longValue(), ((Number) row.get("product_id")).longValue(), fdType.name(),
posting.postingDate(), posting.interest(), (BigDecimal) row.get("balance"),
(BigDecimal) row.get("deposit_amount"), (BigDecimal) row.get("interest_rate"),
toLocalDate(row.get("maturity_date")), payoutId == null ? null : ((Number) payoutId).longValue(),
paysOut ? (String) row.get("payout_account_no") : null));
paysOut ? (String) row.get("payout_account_no") : null, periodFrom));
}
return lines;
}
/**
* Each deposit's most recent posted interest, as at the earliest of its postings being reported
* here — null for a deposit with none, which starts its first period from activation instead.
*/
private Map<Long, LocalDate> previousPeriodEnds(final Set<Long> accountIds, final List<ComputedPosting> postings) {
if (accountIds.isEmpty()) {
return new LinkedHashMap<>();
}
final Map<Long, LocalDate> earliestNew = new LinkedHashMap<>();
for (final ComputedPosting posting : postings) {
earliestNew.merge(posting.accountId(), posting.postingDate(), (a, b) -> a.isBefore(b) ? a : b);
}
final String ids = accountIds.stream().map(String::valueOf).collect(Collectors.joining(","));
final Map<Long, LocalDate> result = new LinkedHashMap<>();
for (final Map<String, Object> row : this.jdbcTemplate.queryForList("""
SELECT savings_account_id, MAX(transaction_date) AS last_date
FROM m_savings_account_transaction
WHERE savings_account_id IN (%s) AND transaction_type_enum = 3 AND is_reversed = false
GROUP BY savings_account_id
""".formatted(ids))) {
final Long accountId = ((Number) row.get("savings_account_id")).longValue();
final LocalDate lastDate = toLocalDate(row.get("last_date"));
final LocalDate newEarliest = earliestNew.get(accountId);
if (lastDate != null && (newEarliest == null || lastDate.isBefore(newEarliest))) {
result.put(accountId, lastDate);
}
}
return result;
}
private static LocalDate toLocalDate(final Object value) {
return value instanceof java.sql.Date date ? date.toLocalDate() : null;
}
@@ -467,11 +523,11 @@ public class FixedDepositInterestPostingReportService {
INSERT INTO m_fd_interest_posting_report_line
(report_id, fd_account_id, account_no, client_name, office_id, interest_posting_date, interest_amount,
balance_before, balance_after, deposit_amount, interest_rate, maturity_date, payout_savings_account_id,
payout_account_no)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
payout_account_no, interest_period_from)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", reportId, line.accountId(), line.accountNo(), line.clientName(), line.officeId(), line.postingDate(), interest,
before, after, line.depositAmount(), line.interestRate(), line.maturityDate(), line.payoutSavingsAccountId(),
line.payoutAccountNo());
line.payoutAccountNo(), line.periodFrom());
}
log.info("FD interest report {}: product {} ({}) branch {} on {} ({}) — {} across {} posting(s)", reportId, productId, fdType,
@@ -624,14 +680,15 @@ public class FixedDepositInterestPostingReportService {
r.total_interest AS "totalInterest", r.account_count AS "accountCount",
r.generation_mode AS "generationMode", r.generated_at AS "generatedAt", gu.username AS "generatedBy",
r.generated_by AS "generatedById", r.approved_at AS "approvedAt", au.username AS "approvedBy",
r.approved_on_behalf AS "approvedOnBehalf", r.posted_at AS "postedAt", r.failure_reason AS "failureReason"
r.approved_on_behalf AS "approvedOnBehalf", r.posted_at AS "postedAt", r.failure_reason AS "failureReason",
%s AS "postingPeriodLabel"
FROM m_fd_interest_posting_report r
JOIN m_savings_product p ON p.id = r.fd_product_id
LEFT JOIN m_office o ON o.id = r.office_id
LEFT JOIN m_appuser gu ON gu.id = r.generated_by
LEFT JOIN m_appuser au ON au.id = r.approved_by
WHERE 1 = 1
""");
""".formatted(POSTING_PERIOD_LABEL_CASE));
final List<Object> params = new ArrayList<>();
if (postingDate != null) {
sql.append(" AND r.posting_date = ?");
@@ -687,7 +744,8 @@ public class FixedDepositInterestPostingReportService {
r.approved_at AS "approvedAt", au.username AS "approvedBy", r.approved_on_behalf AS "approvedOnBehalf",
r.posted_at AS "postedAt", r.failure_reason AS "failureReason",
pg.gl_code AS "payableGlCode", pg.name AS "payableGlName",
dg.gl_code AS "paidGlCode", dg.name AS "paidGlName"
dg.gl_code AS "paidGlCode", dg.name AS "paidGlName",
%s AS "postingPeriodLabel"
FROM m_fd_interest_posting_report r
JOIN m_savings_product p ON p.id = r.fd_product_id
LEFT JOIN m_office o ON o.id = r.office_id
@@ -696,7 +754,7 @@ public class FixedDepositInterestPostingReportService {
LEFT JOIN acc_gl_account pg ON pg.id = r.payable_gl_account_id
LEFT JOIN acc_gl_account dg ON dg.id = r.paid_gl_account_id
WHERE r.id = ?
""", reportId);
""".formatted(POSTING_PERIOD_LABEL_CASE), reportId);
if (found.isEmpty()) {
throw new PlatformDataIntegrityException("error.msg.fdinterestreport.not.found",
"Fixed deposit interest posting report " + reportId + " does not exist.");
@@ -704,6 +762,7 @@ public class FixedDepositInterestPostingReportService {
final Map<String, Object> report = new LinkedHashMap<>(found.get(0));
report.put("lines", this.jdbcTemplate.queryForList("""
SELECT l.fd_account_id AS "fdAccountId", l.account_no AS "accountNo", l.client_name AS "clientName",
l.interest_period_from AS "interestPeriodFrom",
l.interest_posting_date AS "interestPostingDate", l.interest_amount AS "interestAmount",
l.balance_before AS "balanceBefore", l.balance_after AS "balanceAfter",
l.deposit_amount AS "depositAmount", l.interest_rate AS "interestRate", l.maturity_date AS "maturityDate",
@@ -737,6 +796,7 @@ public class FixedDepositInterestPostingReportService {
row.put("productId", line.productId());
row.put("productName", productName(line.productId()));
row.put("fdType", line.fdType());
row.put("interestPeriodFrom", line.periodFrom() == null ? null : line.periodFrom().toString());
row.put("interestPostingDate", line.postingDate() == null ? null : line.postingDate().toString());
row.put("depositAmount", line.depositAmount());
row.put("interestRate", line.interestRate());
@@ -759,13 +819,14 @@ public class FixedDepositInterestPostingReportService {
r.fd_product_id AS "productId", p.name AS "productName", r.fd_type AS "fdType",
r.account_count AS "accountCount", r.total_interest AS "totalInterest", r.status,
au.username AS "approvedBy", r.approved_on_behalf AS "approvedOnBehalf",
r.approved_at AS "approvedAt", r.posted_at AS "postedAt"
r.approved_at AS "approvedAt", r.posted_at AS "postedAt",
%s AS "postingPeriodLabel"
FROM m_fd_interest_posting_report r
JOIN m_savings_product p ON p.id = r.fd_product_id
LEFT JOIN m_office o ON o.id = r.office_id
LEFT JOIN m_appuser au ON au.id = r.approved_by
WHERE r.status <> 'SUPERSEDED'
""");
""".formatted(POSTING_PERIOD_LABEL_CASE));
final List<Object> params = new ArrayList<>();
if (fromDate != null) {
sql.append(" AND r.posting_date >= ?");
@@ -825,6 +886,63 @@ public class FixedDepositInterestPostingReportService {
return out;
}
/**
* Fixed deposit interest posted without ever going through this report — chiefly a deposit maturing
* before its own posting cycle completed, whose last (and only its last) period is settled directly
* by the maturity job rather than reported and approved. Read-only, for visibility: a branch should
* be able to see every rupee of deposit interest it has paid, not only the part that was approved.
*
* <p>
* Found by what it is NOT, rather than by naming the maturity job specifically: any interest
* posting transaction with no report line pointing at it. That also catches a posting from any
* other direct path this or a future change might open, rather than a list that quietly goes stale
* the day a second such path appears.
*
* @param officeId
* one branch, or null for every branch (or every branch's own, under scope)
*/
public List<Map<String, Object>> outsideApproval(final LocalDate fromDate, final LocalDate toDate, final Long officeId,
final AppUser user, final String scope) {
final Long userOffice = officeOf(user);
final boolean others = "others".equalsIgnoreCase(scope) && this.branchAuthority.isHeadOffice(userOffice);
final Long branch = others ? officeId : userOffice;
final StringBuilder sql = new StringBuilder("""
SELECT sat.id AS "transactionId", sa.id AS "fdAccountId", sa.account_no AS "accountNo",
COALESCE(c.display_name, g.display_name) AS "clientName",
COALESCE(c.office_id, g.office_id) AS "officeId", o.name AS "officeName",
sa.product_id AS "productId", sp.name AS "productName", sp.fd_type AS "fdType",
sat.transaction_date AS "interestFor", sat.amount AS "interestAmount",
sat.created_on_utc AS "postedAt"
FROM m_savings_account_transaction sat
JOIN m_savings_account sa ON sa.id = sat.savings_account_id
JOIN m_savings_product sp ON sp.id = sa.product_id
LEFT JOIN m_client c ON c.id = sa.client_id
LEFT JOIN m_group g ON g.id = sa.group_id
LEFT JOIN m_office o ON o.id = COALESCE(c.office_id, g.office_id)
WHERE sat.transaction_type_enum = 3 AND sa.deposit_type_enum = 200 AND sat.is_reversed = false
AND NOT EXISTS (SELECT 1 FROM m_fd_interest_posting_report_line l WHERE l.fd_transaction_id = sat.id)
""");
final List<Object> params = new ArrayList<>();
if (fromDate != null) {
sql.append(" AND sat.transaction_date >= ?");
params.add(fromDate);
}
if (toDate != null) {
sql.append(" AND sat.transaction_date <= ?");
params.add(toDate);
}
if (branch != null) {
sql.append(" AND COALESCE(c.office_id, g.office_id) = ?");
params.add(branch);
} else if (others) {
sql.append(" AND COALESCE(c.office_id, g.office_id) <> ?");
params.add(userOffice);
}
sql.append(" ORDER BY sat.transaction_date DESC, sa.account_no");
return this.jdbcTemplate.queryForList(sql.toString(), params.toArray());
}
// ── who may act ──────────────────────────────────────────────────────────────────────────────
/**
@@ -74,6 +74,15 @@ import org.springframework.transaction.annotation.Transactional;
* and the sweep is left where it already works.
*
* <p>
* A deposit can mature between its report being generated and being posted, and a matured deposit's
* last period is settled directly by the maturity job the moment it matures — see
* {@code FixedDepositAccount#postMaturityInterest}, which this report's generation step cannot see
* coming and does not block on. Posting checks for that first: a line whose deposit already carries an
* interest posting for its exact date is linked to what actually paid it rather than paid again, and
* its amount is reconciled off the report's approved total rather than failing the whole posting over
* money that was never going to move twice.
*
* <p>
* The whole report settles in one transaction: a failure leaves nobody half paid and the report still
* APPROVED for the next attempt.
*/
@@ -118,6 +127,16 @@ public class FixedDepositInterestPostingService {
*/
private record PaidLine(Long accountId, BigDecimal amount, LocalDate interestDate, Long transactionId) {}
/**
* What one line's posting came to: either a new credit to pool with the rest of its date-group, or
* an amount that turned out to be settled already — a deposit that matured, and had this exact
* period's interest posted directly by the maturity job, between this report being generated and
* being posted. {@code paid} is null in that case, and {@code reconciledAway} carries the amount so
* the report's approved total can be reconciled down by it rather than the whole posting failing a
* balance check over money that was never going to move twice.
*/
private record DepositOutcome(PaidLine paid, BigDecimal reconciledAway) {}
/**
* The payable and paid accounts as the product maps them now, read when a report is generated so a
* product missing one is refused before anybody approves figures that could never be posted.
@@ -206,20 +225,25 @@ public class FixedDepositInterestPostingService {
// than crediting one period's money under the other's date.
final Map<LocalDate, List<PaidLine>> byDate = new LinkedHashMap<>();
BigDecimal distributed = BigDecimal.ZERO;
BigDecimal reconciledAway = BigDecimal.ZERO;
for (final Map<String, Object> line : lines) {
final PaidLine paid = payDeposit(line, officeId, postedOn);
if (paid == null) {
continue;
final DepositOutcome outcome = payDeposit(line, officeId, postedOn);
if (outcome.paid() != null) {
byDate.computeIfAbsent(outcome.paid().interestDate(), key -> new ArrayList<>()).add(outcome.paid());
distributed = distributed.add(outcome.paid().amount());
}
byDate.computeIfAbsent(paid.interestDate(), key -> new ArrayList<>()).add(paid);
distributed = distributed.add(paid.amount());
reconciledAway = reconciledAway.add(outcome.reconciledAway());
}
// If what was paid does not add up to what was approved, nothing commits.
if (approved.compareTo(distributed) != 0) {
throw new IllegalStateException(String.format(
"Fixed deposit interest posting report %d did not balance: approved %s, distributed %s", reportId, approved,
distributed));
// A line reconciled away was never going to move twice: its amount is taken off what this
// posting is expected to distribute rather than failing the whole report over money that is
// already in the customer's deposit by another, older path. What was actually paid here still
// has to add up exactly to what is left.
final BigDecimal expected = approved.subtract(reconciledAway);
if (expected.compareTo(distributed) != 0) {
throw new IllegalStateException(
String.format("Fixed deposit interest posting report %d did not balance: approved %s, reconciled away %s,"
+ " distributed %s", reportId, approved, reconciledAway, distributed));
}
for (final Map.Entry<LocalDate, List<PaidLine>> group : byDate.entrySet()) {
@@ -242,14 +266,20 @@ public class FixedDepositInterestPostingService {
* be invisible to it and the same period would be computed and paid again at the next run. The
* close date is kept as the submitted-on stamp, which is what the scheduler has always done.
*
* @return the deposit's credit, or null for a line with nothing to pay
* <p>
* Checked first against the deposit's own transactions for one this report did not put there: a
* deposit can mature between its report being generated and being posted, and the maturity job
* settles a matured deposit's last period itself, directly, the moment it matures — see
* {@code FixedDepositAccount#postMaturityInterest}. Where that has already happened for this exact
* date, the line is linked to what actually paid it rather than paying it again.
*/
private PaidLine payDeposit(final Map<String, Object> line, final long officeId, final LocalDate postedOn) {
private DepositOutcome payDeposit(final Map<String, Object> line, final long officeId, final LocalDate postedOn) {
final BigDecimal amount = ((BigDecimal) line.get("interest_amount")).setScale(MONEY_SCALE, MONEY_ROUNDING);
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return null;
}
final long accountId = ((Number) line.get("fd_account_id")).longValue();
final long lineId = ((Number) line.get("line_id")).longValue();
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return new DepositOutcome(null, BigDecimal.ZERO);
}
final LocalDate interestDate = ((java.sql.Date) line.get("interest_posting_date")).toLocalDate();
// A resumed or duplicated attempt must not pay anybody twice. The line itself records the
@@ -258,6 +288,16 @@ public class FixedDepositInterestPostingService {
throw new IllegalStateException("Fixed deposit " + accountId + " was already paid by this report");
}
final Long existing = findExistingInterestPosting(accountId, interestDate);
if (existing != null) {
log.warn(
"FD interest report line {}: deposit {} already has an interest posting for {} (transaction {}) not raised by this"
+ " report — most likely matured in the meantime. Linking to it instead of posting again.",
lineId, accountId, interestDate, existing);
this.jdbcTemplate.update("UPDATE m_fd_interest_posting_report_line SET fd_transaction_id = ? WHERE id = ?", existing, lineId);
return new DepositOutcome(null, amount);
}
// The balance as it stands now, not as the report showed it: money may have moved since the
// report was generated, and the transaction's running balance has to follow the deposit.
final BigDecimal currentBalance = this.jdbcTemplate.queryForObject(
@@ -283,9 +323,19 @@ public class FixedDepositInterestPostingService {
""", amount, amount, amount, interestDate, interestDate, accountId);
this.jdbcTemplate.update("UPDATE m_fd_interest_posting_report_line SET fd_transaction_id = ? WHERE id = ?", transactionId,
((Number) line.get("line_id")).longValue());
lineId);
return new PaidLine(accountId, amount, interestDate, transactionId);
return new DepositOutcome(new PaidLine(accountId, amount, interestDate, transactionId), BigDecimal.ZERO);
}
/** A non-reversed interest posting this deposit already carries for this exact date, if any. */
private Long findExistingInterestPosting(final long accountId, final LocalDate interestDate) {
final List<Long> found = this.jdbcTemplate.queryForList("""
SELECT id FROM m_savings_account_transaction
WHERE savings_account_id = ? AND transaction_type_enum = ? AND transaction_date = ? AND is_reversed = false
ORDER BY id DESC LIMIT 1
""", Long.class, accountId, INTEREST_POSTING, interestDate);
return found.isEmpty() ? null : found.get(0);
}
/**
@@ -150,6 +150,22 @@ public class FixedDepositInterestPostingReportApiResource {
.toJson(this.reportService.summary(parseDate(fromDate), parseDate(toDate), productId).stream().map(this::plain).toList());
}
/**
* Deposit interest posted without this report ever seeing it — chiefly a deposit maturing before
* its posting cycle completed, whose last period a different, older path settles directly. Here so
* a branch can see every rupee of deposit interest it has paid, approved or not.
*/
@GET
@Path("/outside-approval")
public String outsideApproval(@QueryParam("fromDate") final String fromDate, @QueryParam("toDate") final String toDate,
@QueryParam("officeId") final Long officeId, @QueryParam("scope") final String scope) {
final AppUser user = this.context.authenticatedUser();
user.validateHasReadPermission(RESOURCE);
return this.gson.toJson(
this.reportService.outsideApproval(parseDate(fromDate), parseDate(toDate), officeId, user, scope).stream()
.map(this::plain).toList());
}
@GET
@Path("/{reportId}")
public String detail(@PathParam("reportId") final Long reportId) {
@@ -317,6 +317,21 @@
new path can return to the old one without a code change, and so the two never run at once —
every query that selects deposits for posting reads this same flag.
-->
<!--
The period a line's interest was earned over, not just the date it ends on: a checker reading
"Quarterly: 01 Oct - 31 Dec" is told what it is approving in a way a bare end date never said.
Nullable because a line already posted before this column existed has no retroactive start date
and is shown as an open-ended period rather than invented.
-->
<changeSet author="fineract" id="0593-8-fd-interest-posting-report-line-period-from">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_fd_interest_posting_report_line" columnName="interest_period_from"/></not>
</preConditions>
<addColumn tableName="m_fd_interest_posting_report_line">
<column name="interest_period_from" type="DATE"/>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0593-7-fd-interest-posting-approval-configuration">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM c_configuration WHERE name = 'fd-interest-posting-approval'</sqlCheck>