feat(client-accounts): active accounts only, drop balances/shares, add branch and bank details
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Adds branch pincode (new column + API + migration 0538) since it was missing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+132
-81
@@ -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:
|
||||
* <ul>
|
||||
* <li>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.</li>
|
||||
* <li>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).</li>
|
||||
* <li>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.</li>
|
||||
* </ul>
|
||||
*
|
||||
* 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<String, Object> client = resolveClient(identifier, tenant);
|
||||
final Long clientId = ((Number) client.get("id")).longValue();
|
||||
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.query(ACCOUNTS_SQL, new AccountRowMapper(), clientId, clientId, clientId);
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.query(ACCOUNTS_SQL, new AccountRowMapper(), clientId, clientId);
|
||||
|
||||
final Map<String, Object> totalsByCurrency = new LinkedHashMap<>();
|
||||
final Map<String, Integer> countByType = new LinkedHashMap<>();
|
||||
for (final Map<String, Object> row : rows) {
|
||||
final String accountType = (String) row.get("accountType");
|
||||
countByType.merge(accountType, 1, Integer::sum);
|
||||
|
||||
final Object valueObj = row.get("currentValue");
|
||||
if (valueObj instanceof BigDecimal value) {
|
||||
final String currency = (String) row.get("currencyCode");
|
||||
final BigDecimal running = (BigDecimal) totalsByCurrency.getOrDefault(currency, BigDecimal.ZERO);
|
||||
totalsByCurrency.put(currency, running.add(value));
|
||||
}
|
||||
}
|
||||
|
||||
final Long officeId = ((Number) client.get("office_id")).longValue();
|
||||
final Map<String, Object> branch = resolveBranch(officeId);
|
||||
final Map<String, Object> bank = resolveBank();
|
||||
|
||||
final JsonObject res = new JsonObject();
|
||||
// Which bank this response came from -- not decorative. One API key now authenticates
|
||||
// against every tenant (see ApiClientCredentialService), so the ONLY thing that decides
|
||||
@@ -188,15 +211,71 @@ public class ClientAllAccountsApiResource {
|
||||
res.addProperty("clientNo", (String) client.get("account_no"));
|
||||
res.addProperty("clientName", (String) client.get("display_name"));
|
||||
res.addProperty("mobileNo", (String) client.get("mobile_no"));
|
||||
res.addProperty("officeId", ((Number) client.get("office_id")).longValue());
|
||||
res.addProperty("officeId", officeId);
|
||||
res.addProperty("clientStatusEnum", ((Number) client.get("status_enum")).intValue());
|
||||
res.add("branch", gson.toJsonTree(branch));
|
||||
res.add("bank", gson.toJsonTree(bank));
|
||||
res.addProperty("totalAccounts", rows.size());
|
||||
res.add("accountsByType", gson.toJsonTree(countByType));
|
||||
res.add("totalCurrentValueByCurrency", gson.toJsonTree(totalsByCurrency));
|
||||
res.add("accounts", gson.toJsonTree(rows));
|
||||
return gson.toJson(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* An office with no {@code m_office_profile} row yet (see
|
||||
* {@code org.apache.fineract.portfolio.orgsetup.api.BranchProfileApiResource}) is a valid, ordinary
|
||||
* state -- every field beyond {@code officeId}/{@code officeName} just comes back null rather than
|
||||
* this call failing or being skipped.
|
||||
*/
|
||||
private Map<String, Object> resolveBranch(final Long officeId) {
|
||||
final Map<String, Object> row = jdbcTemplate.queryForMap(BRANCH_SQL, officeId);
|
||||
final Map<String, Object> branch = new LinkedHashMap<>();
|
||||
branch.put("officeId", officeId);
|
||||
branch.put("officeName", row.get("office_name"));
|
||||
branch.put("branchCode", row.get("branch_code"));
|
||||
branch.put("branchType", row.get("branch_type"));
|
||||
branch.put("address", row.get("address"));
|
||||
branch.put("pincode", row.get("pincode"));
|
||||
branch.put("ifscCode", row.get("ifsc_code"));
|
||||
branch.put("micrCode", row.get("micr_code"));
|
||||
branch.put("contactPhone", row.get("contact_phone"));
|
||||
branch.put("contactEmail", row.get("contact_email"));
|
||||
return branch;
|
||||
}
|
||||
|
||||
/**
|
||||
* The bank running this tenant is a singleton row (id=1) that only gets created the first time
|
||||
* someone opens the Bank Profile screen (see {@code InstitutionProfileApiResource#ensureRowExists}
|
||||
* -- this endpoint never creates it itself, it only reads). A tenant where nobody has ever opened
|
||||
* that screen yet is a valid, ordinary state, not an error -- {@code bank} just comes back {@code
|
||||
* null} in the response rather than this call failing.
|
||||
*/
|
||||
private Map<String, Object> resolveBank() {
|
||||
final Map<String, Object> row;
|
||||
try {
|
||||
row = jdbcTemplate.queryForMap(BANK_SQL);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
return null;
|
||||
}
|
||||
final Map<String, Object> bank = new LinkedHashMap<>();
|
||||
bank.put("bankName", row.get("bank_name"));
|
||||
bank.put("legalName", row.get("legal_name"));
|
||||
bank.put("shortName", row.get("short_name"));
|
||||
bank.put("bankCode", row.get("bank_code"));
|
||||
bank.put("swiftBic", row.get("swift_bic"));
|
||||
bank.put("addressLine1", row.get("address_line1"));
|
||||
bank.put("addressLine2", row.get("address_line2"));
|
||||
bank.put("city", row.get("city"));
|
||||
bank.put("district", row.get("district"));
|
||||
bank.put("state", row.get("state"));
|
||||
bank.put("pincode", row.get("pincode"));
|
||||
bank.put("contactPhone", row.get("contact_phone"));
|
||||
bank.put("contactEmail", row.get("contact_email"));
|
||||
bank.put("customerCareNumber", row.get("customer_care_number"));
|
||||
bank.put("website", row.get("website"));
|
||||
return bank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the path identifier as a client number first, then a mobile number -- two lookups in
|
||||
* the worst case, one in the common case, both on unique indexed columns
|
||||
@@ -275,27 +354,19 @@ public class ClientAllAccountsApiResource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps one UNION leg row into a flat, display-ready shape -- category label, human status, and a
|
||||
* single {@code currentValue} figure so the caller never has to know that a deposit's value lives
|
||||
* in one column and a share account's lives in another (approved shares × unit price).
|
||||
* Maps one UNION leg row into a flat, display-ready shape -- category label and human status only.
|
||||
* No balance figure of any kind: this is an account listing, not a balance enquiry (see the class
|
||||
* Javadoc).
|
||||
*/
|
||||
private static final class AccountRowMapper implements RowMapper<Map<String, Object>> {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
final Integer depositType = JdbcSupport.getInteger(rs, "deposit_type");
|
||||
final int depositType = rs.getInt("deposit_type");
|
||||
final Integer statusEnum = JdbcSupport.getInteger(rs, "status_enum");
|
||||
final BigDecimal balance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "balance");
|
||||
final BigDecimal onHold = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "on_hold");
|
||||
final Long approvedShares = JdbcSupport.getLong(rs, "approved_shares");
|
||||
final Long pendingShares = JdbcSupport.getLong(rs, "pending_shares");
|
||||
final BigDecimal unitPrice = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "unit_price");
|
||||
|
||||
final boolean isShare = depositType == null;
|
||||
final String accountType = isShare ? "Share Account" : accountTypeFor(depositType);
|
||||
|
||||
final Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("accountType", accountType);
|
||||
row.put("accountType", accountTypeFor(depositType));
|
||||
row.put("accountId", rs.getLong("account_id"));
|
||||
row.put("accountNo", rs.getString("account_no"));
|
||||
row.put("externalId", rs.getString("external_id"));
|
||||
@@ -311,28 +382,8 @@ public class ClientAllAccountsApiResource {
|
||||
row.put("submittedDate", toIsoDateString(JdbcSupport.getLocalDate(rs, "submitted_date")));
|
||||
row.put("activatedDate", toIsoDateString(JdbcSupport.getLocalDate(rs, "activated_date")));
|
||||
|
||||
if (isShare) {
|
||||
final ShareAccountStatusEnumData status = SharesEnumerations.status(statusEnum);
|
||||
row.put("status", status);
|
||||
row.put("approvedShares", approvedShares);
|
||||
row.put("pendingShares", pendingShares);
|
||||
row.put("unitPrice", unitPrice);
|
||||
final BigDecimal shareValue = (approvedShares != null && unitPrice != null)
|
||||
? unitPrice.multiply(BigDecimal.valueOf(approvedShares))
|
||||
: null;
|
||||
row.put("currentValue", shareValue);
|
||||
} else {
|
||||
final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum);
|
||||
row.put("status", status);
|
||||
row.put("balance", balance);
|
||||
row.put("onHoldFunds", onHold);
|
||||
BigDecimal available = balance;
|
||||
if (available != null && onHold != null) {
|
||||
available = available.subtract(onHold);
|
||||
}
|
||||
row.put("availableBalance", available);
|
||||
row.put("currentValue", balance);
|
||||
}
|
||||
final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum);
|
||||
row.put("status", status);
|
||||
return row;
|
||||
}
|
||||
|
||||
|
||||
+8
-6
@@ -98,6 +98,7 @@ public class BranchProfileApiResource {
|
||||
final String branchCode = this.fromJsonHelper.extractStringNamed("branchCode", root);
|
||||
final String branchType = this.fromJsonHelper.extractStringNamed("branchType", root);
|
||||
final String address = this.fromJsonHelper.extractStringNamed("address", root);
|
||||
final String pincode = this.fromJsonHelper.extractStringNamed("pincode", root);
|
||||
final String contactPhone = this.fromJsonHelper.extractStringNamed("contactPhone", root);
|
||||
final String contactEmail = this.fromJsonHelper.extractStringNamed("contactEmail", root);
|
||||
final String workingHoursStart = this.fromJsonHelper.extractStringNamed("workingHoursStart", root);
|
||||
@@ -124,22 +125,22 @@ public class BranchProfileApiResource {
|
||||
if (existing != null && existing > 0) {
|
||||
jdbcTemplate.update("""
|
||||
UPDATE m_office_profile
|
||||
SET branch_code = ?, branch_type = ?, address = ?, contact_phone = ?, contact_email = ?,
|
||||
SET branch_code = ?, branch_type = ?, address = ?, pincode = ?, contact_phone = ?, contact_email = ?,
|
||||
working_hours_start = ?, working_hours_end = ?, working_days = ?,
|
||||
ifsc_code = ?, micr_code = ?, latitude = ?, longitude = ?,
|
||||
updated_by_username = ?, updated_at = ?
|
||||
WHERE office_id = ?
|
||||
""", branchCode, branchType, address, contactPhone, contactEmail, workingHoursStart,
|
||||
""", branchCode, branchType, address, pincode, contactPhone, contactEmail, workingHoursStart,
|
||||
workingHoursEnd, workingDays, ifscCode, micrCode, latitude, longitude, currentUser.getUsername(),
|
||||
Timestamp.from(Instant.now()), officeId);
|
||||
} else {
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO m_office_profile
|
||||
(office_id, branch_code, branch_type, address, contact_phone, contact_email,
|
||||
(office_id, branch_code, branch_type, address, pincode, contact_phone, contact_email,
|
||||
working_hours_start, working_hours_end, working_days, ifsc_code, micr_code,
|
||||
latitude, longitude, updated_by_username, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""", officeId, branchCode, branchType, address, contactPhone, contactEmail, workingHoursStart,
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""", officeId, branchCode, branchType, address, pincode, contactPhone, contactEmail, workingHoursStart,
|
||||
workingHoursEnd, workingDays, ifscCode, micrCode, latitude, longitude, currentUser.getUsername(),
|
||||
Timestamp.from(Instant.now()));
|
||||
}
|
||||
@@ -150,7 +151,7 @@ public class BranchProfileApiResource {
|
||||
private String baseSql() {
|
||||
return """
|
||||
SELECT o.id AS office_id, o.name AS office_name, o.hierarchy, o.parent_id, o.opening_date,
|
||||
p.branch_code, p.branch_type, p.address, p.contact_phone, p.contact_email,
|
||||
p.branch_code, p.branch_type, p.address, p.pincode, p.contact_phone, p.contact_email,
|
||||
p.working_hours_start, p.working_hours_end, p.working_days,
|
||||
p.ifsc_code, p.micr_code, p.latitude, p.longitude,
|
||||
p.updated_by_username, p.updated_at
|
||||
@@ -169,6 +170,7 @@ public class BranchProfileApiResource {
|
||||
out.put("branchCode", row.get("branch_code"));
|
||||
out.put("branchType", row.get("branch_type"));
|
||||
out.put("address", row.get("address"));
|
||||
out.put("pincode", row.get("pincode"));
|
||||
out.put("contactPhone", row.get("contact_phone"));
|
||||
out.put("contactEmail", row.get("contact_email"));
|
||||
out.put("workingHoursStart", row.get("working_hours_start"));
|
||||
|
||||
@@ -64,4 +64,5 @@
|
||||
<include file="parts/0527_clerk_closure_request_voucher_no.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0536_sync_job_identity_sequence.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0537_seed_delete_office_permission.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0538_add_branch_pincode.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?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">
|
||||
|
||||
<!-- 0511's branch profile carries a single free-text address field, unlike the institution
|
||||
profile (0512), which was later split into address_line1/2, city, district, state, pincode.
|
||||
A branch's own pincode is needed on its own (e.g. for a client-facing API returning branch
|
||||
identifiers alongside IFSC/MICR) without asking the caller to parse it back out of a free-text
|
||||
address block: it is added here as its own column instead, the same way IFSC/MICR were added onto
|
||||
this same table in 0512 rather than folded into the address text. -->
|
||||
<changeSet id="0538_add_branch_pincode" author="cbs-nitesh">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_office_profile" columnName="pincode"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_office_profile">
|
||||
<column name="pincode" type="VARCHAR(10)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user