feat(fd): add lean FD maturity dashboard read endpoint

New GET /v1/fixeddepositaccounts/maturity-dashboard: a narrow, purpose-built query for the FD
Dashboard's maturity ladder (account/client/product identity, maturity date, deposit/maturity
amounts) instead of routing through the general FixedDepositAccountsApiResource#retrieveAll,
whose mapper joins six extra tables for audit/currency/tax fields this screen never shows.

Adds the index migration (0571) the dashboard's WHERE clause needs -- neither
m_savings_account.deposit_type_enum/status_enum nor
m_deposit_account_term_and_preclosure.maturity_date had one before, so the endpoint would
otherwise run a full table scan on top of being wide.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 15:54:57 +05:30
co-authored by Claude Sonnet 5
parent 26d4c241c7
commit a7cb09f15c
2 changed files with 132 additions and 0 deletions
@@ -0,0 +1,96 @@
/**
* 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.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
/**
* FD Dashboard's data source: every ACTIVE Fixed Deposit with just the fields the maturity ladder
* needs (account/client/product identity, maturity date, deposit and maturity amounts).
*
* Deliberately not routed through {@link FixedDepositAccountsApiResource#retrieveAll}: that
* endpoint's mapper joins m_savings_account to m_client, m_group, m_staff, m_currency,
* m_code_value and FIVE separate m_appuser joins (submitted/rejected/withdrawn/approved/activated/
* closed-by), plus a correlated subquery against m_savings_account_transaction per row -- all of it
* needed for the single-account and full-list screens it serves, none of it needed here. Neither
* m_savings_account.deposit_type_enum/status_enum nor
* m_deposit_account_term_and_preclosure.maturity_date had an index before this feature (confirmed
* directly against pg_indexes), so that query was a full table scan on top of being wide. This
* endpoint selects only what the dashboard renders, against the narrow, now-indexed WHERE clause
* this feature added (see migration 0571).
*/
@Path("/v1/fixeddepositaccounts/maturity-dashboard")
@Component
@Tag(name = "Fixed Deposit Accounts", description = "FD Dashboard: active FDs positioned against their maturity date.")
@RequiredArgsConstructor
public class FixedDepositMaturityDashboardApiResource {
/** Fineract's DepositAccountType.FIXED_DEPOSIT. */
private static final int FIXED_DEPOSIT = 200;
/** SavingsAccountStatusType.ACTIVE. */
private static final int ACTIVE = 300;
private final PlatformSecurityContext context;
private final JdbcTemplate jdbcTemplate;
private final Gson gson = new Gson();
@GET
@Produces(MediaType.APPLICATION_JSON)
public String retrieve() {
// Same permission as the single-account/full-list FD endpoints -- this is a narrower read of
// the same resource, not a new one, so no new permission is introduced for it.
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME);
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList("""
SELECT sa.id AS "id",
sa.account_no AS "accountNo",
sa.client_id AS "clientId",
c.display_name AS "clientName",
sp.name AS "productName",
sp.fd_type AS "fdType",
TO_CHAR(datp.maturity_date, 'YYYY-MM-DD') AS "maturityDate",
datp.deposit_amount AS "depositAmount",
datp.maturity_amount AS "maturityAmount",
datp.on_account_closure_enum AS "onClosureType",
sa.linked_account_id AS "linkedAccountId",
dptp.renewal_grace_period_days AS "renewalGracePeriodDays"
FROM m_savings_account sa
JOIN m_deposit_account_term_and_preclosure datp ON datp.savings_account_id = sa.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_client c ON c.id = sa.client_id
WHERE sa.deposit_type_enum = ? AND sa.status_enum = ?
ORDER BY datp.maturity_date ASC
""", FIXED_DEPOSIT, ACTIVE);
return this.gson.toJson(rows);
}
}
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
The FD Dashboard's maturity-dashboard endpoint filters m_savings_account by
(deposit_type_enum = 200, status_enum = 300) and orders by
m_deposit_account_term_and_preclosure.maturity_date. Neither column was indexed, confirmed
by querying pg_indexes directly before adding this, so both were full table scans, which
only gets slower as the FD book grows. These two indexes are what actually makes that
endpoint fast; the endpoint's own SQL was already written lean, but a lean query still scans
the whole table without something to seek on.
-->
<changeSet id="0571_index_savings_account_deposit_type_status" author="CBS">
<preConditions onFail="MARK_RAN">
<not><indexExists indexName="idx_msa_deposit_type_status"/></not>
</preConditions>
<createIndex tableName="m_savings_account" indexName="idx_msa_deposit_type_status">
<column name="deposit_type_enum"/>
<column name="status_enum"/>
</createIndex>
</changeSet>
<changeSet id="0571_index_deposit_account_term_preclosure_maturity_date" author="CBS">
<preConditions onFail="MARK_RAN">
<not><indexExists indexName="idx_datp_maturity_date"/></not>
</preConditions>
<createIndex tableName="m_deposit_account_term_and_preclosure" indexName="idx_datp_maturity_date">
<column name="maturity_date"/>
</createIndex>
</changeSet>
</databaseChangeLog>