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

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:
2026-09-09 13:00:12 +05:30
co-authored by Claude Opus 5
parent 05bceea2d4
commit 518b4d4afc
4 changed files with 163 additions and 87 deletions
@@ -32,7 +32,6 @@ import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.math.BigDecimal;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDate; 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.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.savings.data.SavingsAccountStatusEnumData; import org.apache.fineract.portfolio.savings.data.SavingsAccountStatusEnumData;
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountStatusEnumData; import org.springframework.dao.EmptyResultDataAccessException;
import org.apache.fineract.portfolio.shareaccounts.service.SharesEnumerations;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* A single client's full deposit + share picture, looked up by client number OR mobile number (the * A single client's active deposit accounts (Savings/Fixed Deposit/Recurring Deposit/Current -- Share
* path segment is tried as {@code m_client.account_no} first, then {@code m_client.mobile_no}) rather * accounts are out of scope for this endpoint, see below), looked up by client number OR mobile number
* than the internal client id -- these are what a counter clerk has on the slip, or what a third-party * (the path segment is tried as {@code m_client.account_no} first, then {@code m_client.mobile_no})
* caller was given, respectively, so no separate client-search step is needed before this call. Built * rather than the internal client id -- these are what a counter clerk has on the slip, or what a
* so the whole thing, including the client lookup itself, resolves in a small handful of index-scan-only * third-party caller was given, respectively, so no separate client-search step is needed before this
* queries, on the theory that this is exactly the kind of check a teller or manager runs right before * call. Built so the whole thing, including the client lookup itself, resolves in a small handful of
* approving something (a share purchase, a closure, a large withdrawal) -- the "is there a problem here" * index-scan-only queries.
* question this endpoint answers has to come back before that decision is made, not after. *
* 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} * 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 * (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 * 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 * caller doesn't need. Fewer joins and fewer columns is what actually makes this fast, not a different
* whole ask. Fewer joins and fewer columns is what actually makes this fast, not a different query * query engine.
* engine.
* *
* Two ways in, deliberately: a normal Fineract user (Basic Auth, unchanged) can call this exactly as * 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 * 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") @Path("/v1/clientaccounts")
@Component @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 @RequiredArgsConstructor
public class ClientAllAccountsApiResource { public class ClientAllAccountsApiResource {
@@ -96,44 +111,56 @@ public class ClientAllAccountsApiResource {
// that unrelated feature toggle is on. // that unrelated feature toggle is on.
private static final String[] IP_HEADER_CANDIDATES = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP" }; 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), // ACTIVE-only status filter: 300 is SavingsAccountStatusType.ACTIVE, and m_current_account reuses
// CD (m_current_account, its own table -- current accounts were never folded into // the same status_enum numbering (see AccountDetailsReadPlatformServiceJpaRepositoryImpl for the
// m_savings_account here, see AccountDetailsReadPlatformServiceJpaRepositoryImpl for the same // same convention elsewhere in this codebase). One UNION ALL, one round trip: SB/FD/RD
// split), and Share (m_share_account). Every leg filters on client_id, which is indexed on // (m_savings_account, split by deposit_type_enum) and CD (m_current_account, its own table --
// all three tables (fksa00000000000001, idx_ca_client_id, m_share_account_ibfk_9) -- an // 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. // index-only lookup per leg, not a scan.
private static final String ACCOUNTS_SQL = """ 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, 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, sa.status_enum AS status_enum, sa.currency_code, sa.product_id,
p.name AS product_name, p.short_name AS short_product_name, 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 sa.submittedon_date AS submitted_date, sa.activatedon_date AS activated_date
FROM m_savings_account sa FROM m_savings_account sa
JOIN m_savings_product p ON p.id = sa.product_id 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 UNION ALL
SELECT CAST(400 AS smallint) AS deposit_type, ca.id, ca.account_no, ca.external_id, 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.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 ca.submittedon_date, ca.activatedon_date
FROM m_current_account ca FROM m_current_account ca
JOIN m_current_product cp ON cp.id = ca.product_id 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, // The bank itself (m_institution_profile, see InstitutionProfileApiResource) -- a singleton row,
shr.status_enum, shr.currency_code, shr.product_id, sp.name, sp.short_name, // id=1, one per tenant. Only the fields a caller printing a passbook/statement header or routing a
NULL::numeric(19,6), NULL::numeric(19,6), // payment actually needs: identity, SWIFT/BIC (the institution-level counterpart to a branch's
shr.total_approved_shares, shr.total_pending_shares, sp.unit_price, // IFSC/MICR), registered address, and contact. Deliberately leaves out the authorised-representative
shr.submitted_date, shr.activated_date // block and terms-and-conditions text -- regulatory-filing detail, not account-lookup detail -- and
FROM m_share_account shr // the created/updated audit columns, which are an internal concern of the Bank Profile screen, not
JOIN m_share_product sp ON sp.id = shr.product_id // this endpoint's caller.
WHERE shr.client_id = ? 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 @GET
@@ -157,22 +184,18 @@ public class ClientAllAccountsApiResource {
final Map<String, Object> client = resolveClient(identifier, tenant); final Map<String, Object> client = resolveClient(identifier, tenant);
final Long clientId = ((Number) client.get("id")).longValue(); 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<>(); final Map<String, Integer> countByType = new LinkedHashMap<>();
for (final Map<String, Object> row : rows) { for (final Map<String, Object> row : rows) {
final String accountType = (String) row.get("accountType"); final String accountType = (String) row.get("accountType");
countByType.merge(accountType, 1, Integer::sum); 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(); final JsonObject res = new JsonObject();
// Which bank this response came from -- not decorative. One API key now authenticates // Which bank this response came from -- not decorative. One API key now authenticates
// against every tenant (see ApiClientCredentialService), so the ONLY thing that decides // 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("clientNo", (String) client.get("account_no"));
res.addProperty("clientName", (String) client.get("display_name")); res.addProperty("clientName", (String) client.get("display_name"));
res.addProperty("mobileNo", (String) client.get("mobile_no")); 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.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.addProperty("totalAccounts", rows.size());
res.add("accountsByType", gson.toJsonTree(countByType)); res.add("accountsByType", gson.toJsonTree(countByType));
res.add("totalCurrentValueByCurrency", gson.toJsonTree(totalsByCurrency));
res.add("accounts", gson.toJsonTree(rows)); res.add("accounts", gson.toJsonTree(rows));
return gson.toJson(res); 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 * 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 * 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 * Maps one UNION leg row into a flat, display-ready shape -- category label and human status only.
* single {@code currentValue} figure so the caller never has to know that a deposit's value lives * No balance figure of any kind: this is an account listing, not a balance enquiry (see the class
* in one column and a share account's lives in another (approved shares × unit price). * Javadoc).
*/ */
private static final class AccountRowMapper implements RowMapper<Map<String, Object>> { private static final class AccountRowMapper implements RowMapper<Map<String, Object>> {
@Override @Override
public Map<String, Object> mapRow(final ResultSet rs, final int rowNum) throws SQLException { 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 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<>(); 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("accountId", rs.getLong("account_id"));
row.put("accountNo", rs.getString("account_no")); row.put("accountNo", rs.getString("account_no"));
row.put("externalId", rs.getString("external_id")); 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("submittedDate", toIsoDateString(JdbcSupport.getLocalDate(rs, "submitted_date")));
row.put("activatedDate", toIsoDateString(JdbcSupport.getLocalDate(rs, "activated_date"))); row.put("activatedDate", toIsoDateString(JdbcSupport.getLocalDate(rs, "activated_date")));
if (isShare) { final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum);
final ShareAccountStatusEnumData status = SharesEnumerations.status(statusEnum); row.put("status", status);
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);
}
return row; return row;
} }
@@ -98,6 +98,7 @@ public class BranchProfileApiResource {
final String branchCode = this.fromJsonHelper.extractStringNamed("branchCode", root); final String branchCode = this.fromJsonHelper.extractStringNamed("branchCode", root);
final String branchType = this.fromJsonHelper.extractStringNamed("branchType", root); final String branchType = this.fromJsonHelper.extractStringNamed("branchType", root);
final String address = this.fromJsonHelper.extractStringNamed("address", 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 contactPhone = this.fromJsonHelper.extractStringNamed("contactPhone", root);
final String contactEmail = this.fromJsonHelper.extractStringNamed("contactEmail", root); final String contactEmail = this.fromJsonHelper.extractStringNamed("contactEmail", root);
final String workingHoursStart = this.fromJsonHelper.extractStringNamed("workingHoursStart", root); final String workingHoursStart = this.fromJsonHelper.extractStringNamed("workingHoursStart", root);
@@ -124,22 +125,22 @@ public class BranchProfileApiResource {
if (existing != null && existing > 0) { if (existing != null && existing > 0) {
jdbcTemplate.update(""" jdbcTemplate.update("""
UPDATE m_office_profile 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 = ?, working_hours_start = ?, working_hours_end = ?, working_days = ?,
ifsc_code = ?, micr_code = ?, latitude = ?, longitude = ?, ifsc_code = ?, micr_code = ?, latitude = ?, longitude = ?,
updated_by_username = ?, updated_at = ? updated_by_username = ?, updated_at = ?
WHERE office_id = ? WHERE office_id = ?
""", branchCode, branchType, address, contactPhone, contactEmail, workingHoursStart, """, branchCode, branchType, address, pincode, contactPhone, contactEmail, workingHoursStart,
workingHoursEnd, workingDays, ifscCode, micrCode, latitude, longitude, currentUser.getUsername(), workingHoursEnd, workingDays, ifscCode, micrCode, latitude, longitude, currentUser.getUsername(),
Timestamp.from(Instant.now()), officeId); Timestamp.from(Instant.now()), officeId);
} else { } else {
jdbcTemplate.update(""" jdbcTemplate.update("""
INSERT INTO m_office_profile 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, working_hours_start, working_hours_end, working_days, ifsc_code, micr_code,
latitude, longitude, updated_by_username, updated_at) latitude, longitude, updated_by_username, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""", officeId, branchCode, branchType, address, contactPhone, contactEmail, workingHoursStart, """, officeId, branchCode, branchType, address, pincode, contactPhone, contactEmail, workingHoursStart,
workingHoursEnd, workingDays, ifscCode, micrCode, latitude, longitude, currentUser.getUsername(), workingHoursEnd, workingDays, ifscCode, micrCode, latitude, longitude, currentUser.getUsername(),
Timestamp.from(Instant.now())); Timestamp.from(Instant.now()));
} }
@@ -150,7 +151,7 @@ public class BranchProfileApiResource {
private String baseSql() { private String baseSql() {
return """ return """
SELECT o.id AS office_id, o.name AS office_name, o.hierarchy, o.parent_id, o.opening_date, 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.working_hours_start, p.working_hours_end, p.working_days,
p.ifsc_code, p.micr_code, p.latitude, p.longitude, p.ifsc_code, p.micr_code, p.latitude, p.longitude,
p.updated_by_username, p.updated_at p.updated_by_username, p.updated_at
@@ -169,6 +170,7 @@ public class BranchProfileApiResource {
out.put("branchCode", row.get("branch_code")); out.put("branchCode", row.get("branch_code"));
out.put("branchType", row.get("branch_type")); out.put("branchType", row.get("branch_type"));
out.put("address", row.get("address")); out.put("address", row.get("address"));
out.put("pincode", row.get("pincode"));
out.put("contactPhone", row.get("contact_phone")); out.put("contactPhone", row.get("contact_phone"));
out.put("contactEmail", row.get("contact_email")); out.put("contactEmail", row.get("contact_email"));
out.put("workingHoursStart", row.get("working_hours_start")); 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/0527_clerk_closure_request_voucher_no.xml" relativeToChangelogFile="true"/>
<include file="parts/0536_sync_job_identity_sequence.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/0537_seed_delete_office_permission.xml" relativeToChangelogFile="true"/>
<include file="parts/0538_add_branch_pincode.xml" relativeToChangelogFile="true"/>
</databaseChangeLog> </databaseChangeLog>
@@ -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>