diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientAllAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientAllAccountsApiResource.java
index e7c7d6c37..67b91199b 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientAllAccountsApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientAllAccountsApiResource.java
@@ -32,7 +32,6 @@ import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
-import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
@@ -48,28 +47,44 @@ import org.apache.fineract.infrastructure.security.apiclient.ApiClientCredential
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.savings.data.SavingsAccountStatusEnumData;
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
-import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountStatusEnumData;
-import org.apache.fineract.portfolio.shareaccounts.service.SharesEnumerations;
+import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
/**
- * A single client's full deposit + share picture, looked up by client number OR mobile number (the
- * path segment is tried as {@code m_client.account_no} first, then {@code m_client.mobile_no}) rather
- * than the internal client id -- these are what a counter clerk has on the slip, or what a third-party
- * caller was given, respectively, so no separate client-search step is needed before this call. Built
- * so the whole thing, including the client lookup itself, resolves in a small handful of index-scan-only
- * queries, on the theory that this is exactly the kind of check a teller or manager runs right before
- * approving something (a share purchase, a closure, a large withdrawal) -- the "is there a problem here"
- * question this endpoint answers has to come back before that decision is made, not after.
+ * A single client's active deposit accounts (Savings/Fixed Deposit/Recurring Deposit/Current -- Share
+ * accounts are out of scope for this endpoint, see below), looked up by client number OR mobile number
+ * (the path segment is tried as {@code m_client.account_no} first, then {@code m_client.mobile_no})
+ * rather than the internal client id -- these are what a counter clerk has on the slip, or what a
+ * third-party caller was given, respectively, so no separate client-search step is needed before this
+ * call. Built so the whole thing, including the client lookup itself, resolves in a small handful of
+ * index-scan-only queries.
+ *
+ * Deliberately narrow on purpose:
+ *
+ * - Only ACTIVE accounts (status_enum 300) are returned -- a closed, pending, or rejected account is
+ * not part of "what does this client currently hold", which is the question this endpoint answers.
+ * - No balance/currentValue/onHoldFunds figures at all, for any account type -- this is an account
+ * listing, not a balance enquiry; a caller that needs the money figure is expected to hit the
+ * account-specific balance endpoint for it (kept as a smaller, separate, cacheable concern rather than
+ * mixed into every listing response).
+ * - Share accounts are excluded entirely -- they carry no balance in the deposit sense (approved
+ * shares x unit price is a valuation, not a balance) and don't belong in a "deposit accounts" listing.
+ *
+ *
+ * Branch details (name, code, address, pincode, IFSC, MICR -- see {@code m_office_profile}, migrations
+ * 0511-0513/0538) and bank details (name, SWIFT/BIC, registered address, contact -- see
+ * {@code m_institution_profile}) are included on every response: a caller resolving "which branch/bank
+ * is this client's account actually at" (for a passbook header, a cheque book request, a KYC document)
+ * would otherwise need two more round trips, to {@code /v1/branchprofile/{officeId}} and
+ * {@code /v1/institutionprofile}.
*
* Deliberately its own lightweight resource rather than a reuse of {@code AccountSummaryCollectionData}
* (GET /clients/{clientId}/accounts): that endpoint's row mappers each carry six LEFT JOINs to
* m_appuser for a full submitted/approved/rejected/activated/closed audit trail per account, which this
- * caller doesn't need -- account number, product, status and the current balance/share value is the
- * whole ask. Fewer joins and fewer columns is what actually makes this fast, not a different query
- * engine.
+ * caller doesn't need. Fewer joins and fewer columns is what actually makes this fast, not a different
+ * query engine.
*
* Two ways in, deliberately: a normal Fineract user (Basic Auth, unchanged) can call this exactly as
* they do every other endpoint; a third-party integration that should never see a Fineract username or
@@ -81,7 +96,7 @@ import org.springframework.stereotype.Component;
*/
@Path("/v1/clientaccounts")
@Component
-@Tag(name = "Client All Accounts", description = "Fast, single-lookup view of every Savings/Fixed Deposit/Recurring Deposit/Current/Share account a client holds, by client number or mobile number.")
+@Tag(name = "Client All Accounts", description = "Fast, single-lookup view of a client's ACTIVE Savings/Fixed Deposit/Recurring Deposit/Current accounts (no balances, no Share accounts) plus their branch details, by client number or mobile number.")
@RequiredArgsConstructor
public class ClientAllAccountsApiResource {
@@ -96,44 +111,56 @@ public class ClientAllAccountsApiResource {
// that unrelated feature toggle is on.
private static final String[] IP_HEADER_CANDIDATES = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP" };
- // One UNION ALL, one round trip: SB/FD/RD (m_savings_account, split by deposit_type_enum),
- // CD (m_current_account, its own table -- current accounts were never folded into
- // m_savings_account here, see AccountDetailsReadPlatformServiceJpaRepositoryImpl for the same
- // split), and Share (m_share_account). Every leg filters on client_id, which is indexed on
- // all three tables (fksa00000000000001, idx_ca_client_id, m_share_account_ibfk_9) -- an
+ // ACTIVE-only status filter: 300 is SavingsAccountStatusType.ACTIVE, and m_current_account reuses
+ // the same status_enum numbering (see AccountDetailsReadPlatformServiceJpaRepositoryImpl for the
+ // same convention elsewhere in this codebase). One UNION ALL, one round trip: SB/FD/RD
+ // (m_savings_account, split by deposit_type_enum) and CD (m_current_account, its own table --
+ // current accounts were never folded into m_savings_account here). Every leg filters on
+ // client_id, which is indexed on both tables (fksa00000000000001, idx_ca_client_id) -- an
// index-only lookup per leg, not a scan.
private static final String ACCOUNTS_SQL = """
SELECT sa.deposit_type_enum AS deposit_type, sa.id AS account_id, sa.account_no, sa.external_id,
sa.status_enum AS status_enum, sa.currency_code, sa.product_id,
p.name AS product_name, p.short_name AS short_product_name,
- sa.account_balance_derived AS balance, COALESCE(sa.on_hold_funds_derived, 0) AS on_hold,
- NULL::bigint AS approved_shares, NULL::bigint AS pending_shares, NULL::numeric(10,2) AS unit_price,
sa.submittedon_date AS submitted_date, sa.activatedon_date AS activated_date
FROM m_savings_account sa
JOIN m_savings_product p ON p.id = sa.product_id
- WHERE sa.client_id = ?
+ WHERE sa.client_id = ? AND sa.status_enum = 300
UNION ALL
SELECT CAST(400 AS smallint) AS deposit_type, ca.id, ca.account_no, ca.external_id,
ca.status_enum, ca.currency_code, ca.product_id, cp.name, cp.short_name,
- ca.account_balance_derived, COALESCE(ca.on_hold_funds_derived, 0),
- NULL::bigint, NULL::bigint, NULL::numeric(10,2),
ca.submittedon_date, ca.activatedon_date
FROM m_current_account ca
JOIN m_current_product cp ON cp.id = ca.product_id
- WHERE ca.client_id = ?
+ WHERE ca.client_id = ? AND ca.status_enum = 300
+ """;
- UNION ALL
+ // Branch details for the client's own office -- name, code, address, pincode, IFSC, MICR -- so a
+ // caller doesn't need a second round trip to /v1/branchprofile/{officeId} just to print a passbook
+ // header or route a cheque book request. LEFT JOIN: an office with no m_office_profile row yet
+ // (see BranchProfileApiResource) is a valid, ordinary state, not an error -- the profile fields
+ // just come back null.
+ private static final String BRANCH_SQL = """
+ SELECT o.id AS office_id, o.name AS office_name, p.branch_code, p.branch_type, p.address,
+ p.pincode, p.ifsc_code, p.micr_code, p.contact_phone, p.contact_email
+ FROM m_office o LEFT JOIN m_office_profile p ON p.office_id = o.id
+ WHERE o.id = ?
+ """;
- SELECT CAST(NULL AS smallint) AS deposit_type, shr.id, shr.account_no, shr.external_id,
- shr.status_enum, shr.currency_code, shr.product_id, sp.name, sp.short_name,
- NULL::numeric(19,6), NULL::numeric(19,6),
- shr.total_approved_shares, shr.total_pending_shares, sp.unit_price,
- shr.submitted_date, shr.activated_date
- FROM m_share_account shr
- JOIN m_share_product sp ON sp.id = shr.product_id
- WHERE shr.client_id = ?
+ // The bank itself (m_institution_profile, see InstitutionProfileApiResource) -- a singleton row,
+ // id=1, one per tenant. Only the fields a caller printing a passbook/statement header or routing a
+ // payment actually needs: identity, SWIFT/BIC (the institution-level counterpart to a branch's
+ // IFSC/MICR), registered address, and contact. Deliberately leaves out the authorised-representative
+ // block and terms-and-conditions text -- regulatory-filing detail, not account-lookup detail -- and
+ // the created/updated audit columns, which are an internal concern of the Bank Profile screen, not
+ // this endpoint's caller.
+ private static final String BANK_SQL = """
+ SELECT bank_name, legal_name, short_name, bank_code, swift_bic,
+ address_line1, address_line2, city, district, state, pincode,
+ contact_phone, contact_email, customer_care_number, website
+ FROM m_institution_profile WHERE id = 1
""";
@GET
@@ -157,22 +184,18 @@ public class ClientAllAccountsApiResource {
final Map client = resolveClient(identifier, tenant);
final Long clientId = ((Number) client.get("id")).longValue();
- final List