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.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;
}
@@ -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>
@@ -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>