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:
+96
@@ -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);
|
||||
}
|
||||
}
|
||||
+36
@@ -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>
|
||||
Reference in New Issue
Block a user