Client uniqueness pre-save check + seed all Indian states & UTs

Pre-save validator (ClientWritePlatformServiceJpaRepositoryImpl.validateClientUniqueness)
rejects duplicate mobile / email / PAN / Aadhaar (masked) / GSTIN before saveAndFlush
and before the LincPay KYC API call. Avoids burning vendor credits on identity-duplicate
records and gives the UI a clean field-level 403 instead of waiting for the DB constraint.

Splits the new repository finders into "create" and "excluding-self" variants so a
nullable Long parameter never reaches PostgreSQL's parser (same bigint <> varchar trap
that broke the family-member POST earlier).

Also seeds the full set of 28 Indian States + 8 UTs as STATE code_values (migration
0297). Previously only MAHARASHTRA / MADHYA_PRADESH / DELHI existed and the names were
UPPER_SNAKE_CASE; the postal PIN-lookup service returns "Madhya Pradesh" so any state
other than Maharashtra/Delhi silently failed to resolve to m_address.state_province_id.
Renames the 3 existing rows in place (preserves their ids/FKs) and inserts the missing
33 with idempotent preConditions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-20 12:31:54 +05:30
co-authored by Claude Opus 4.7
parent 026e5fb875
commit 8b4351013e
5 changed files with 446 additions and 0 deletions
@@ -35,4 +35,31 @@ public interface ClientKycRepository extends JpaRepository<ClientKyc, Long> {
@Query("SELECT k FROM ClientKyc k WHERE k.kycStatus = :status") @Query("SELECT k FROM ClientKyc k WHERE k.kycStatus = :status")
List<ClientKyc> findAllByStatus(@Param("status") Integer status); List<ClientKyc> findAllByStatus(@Param("status") Integer status);
// ── Pre-KYC uniqueness probes ───────────────────────────────────────────
// Why: stop a second client from being created with the same PAN / Aadhaar / GSTIN BEFORE the
// external KYC API is called, so we don't burn vendor calls on duplicate-identity records. PAN
// and GSTIN match exact upper-cased; Aadhaar is compared masked-form (only last 4 are stored).
// Two variants per field — one for create (no row to exclude), one for update (exclude self) —
// to avoid PostgreSQL's bigint <> varchar parse error on a null Long parameter.
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k WHERE UPPER(k.panNumber) = UPPER(:panNumber)")
boolean existsByPanNumber(@Param("panNumber") String panNumber);
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k "
+ "WHERE UPPER(k.panNumber) = UPPER(:panNumber) AND k.client.id <> :excludeClientId")
boolean existsByPanNumberExcluding(@Param("panNumber") String panNumber, @Param("excludeClientId") Long excludeClientId);
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k WHERE k.aadhaarNumberMasked = :aadhaarMasked")
boolean existsByAadhaarMasked(@Param("aadhaarMasked") String aadhaarMasked);
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k "
+ "WHERE k.aadhaarNumberMasked = :aadhaarMasked AND k.client.id <> :excludeClientId")
boolean existsByAadhaarMaskedExcluding(@Param("aadhaarMasked") String aadhaarMasked, @Param("excludeClientId") Long excludeClientId);
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k WHERE UPPER(k.gstinNumber) = UPPER(:gstinNumber)")
boolean existsByGstinNumber(@Param("gstinNumber") String gstinNumber);
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k "
+ "WHERE UPPER(k.gstinNumber) = UPPER(:gstinNumber) AND k.client.id <> :excludeClientId")
boolean existsByGstinNumberExcluding(@Param("gstinNumber") String gstinNumber, @Param("excludeClientId") Long excludeClientId);
} }
@@ -61,4 +61,25 @@ public interface ClientRepository extends JpaRepository<Client, Long>, JpaSpecif
*/ */
@Query("SELECT COUNT(c) FROM Client c WHERE c.cifNumber LIKE CONCAT(:prefix, '%')") @Query("SELECT COUNT(c) FROM Client c WHERE c.cifNumber LIKE CONCAT(:prefix, '%')")
long countByCifNumberPrefix(@Param("prefix") String prefix); long countByCifNumberPrefix(@Param("prefix") String prefix);
// ── Pre-save uniqueness probes ───────────────────────────────────────────
// Why: DB has UNIQUE on mobile_no / email_address, but we want to fail fast with a clear
// field-level error BEFORE saveAndFlush — and importantly before the external KYC API call.
// Two variants on each: create-time has no row to exclude; update-time excludes the row being
// edited so it doesn't conflict with itself. Splitting avoids the bigint <> varchar JPQL bug
// PostgreSQL throws when a null Long parameter is parsed as varchar at prepare-time.
@Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM Client c WHERE c.mobileNo = :mobileNo")
boolean existsByMobileNo(@Param("mobileNo") String mobileNo);
@Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM Client c "
+ "WHERE c.mobileNo = :mobileNo AND c.id <> :excludeClientId")
boolean existsByMobileNoExcluding(@Param("mobileNo") String mobileNo, @Param("excludeClientId") Long excludeClientId);
@Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM Client c "
+ "WHERE LOWER(c.emailAddress) = LOWER(:emailAddress)")
boolean existsByEmailAddress(@Param("emailAddress") String emailAddress);
@Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM Client c "
+ "WHERE LOWER(c.emailAddress) = LOWER(:emailAddress) AND c.id <> :excludeClientId")
boolean existsByEmailAddressExcluding(@Param("emailAddress") String emailAddress, @Param("excludeClientId") Long excludeClientId);
} }
@@ -67,7 +67,9 @@ import org.apache.fineract.portfolio.client.data.ClientDataValidator;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientEnumerations; import org.apache.fineract.portfolio.client.domain.ClientEnumerations;
import org.apache.fineract.portfolio.client.domain.ClientNonPerson; import org.apache.fineract.portfolio.client.domain.ClientNonPerson;
import org.apache.fineract.portfolio.client.domain.ClientKycRepository;
import org.apache.fineract.portfolio.client.domain.ClientNonPersonRepositoryWrapper; import org.apache.fineract.portfolio.client.domain.ClientNonPersonRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientRepository;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientStatus; import org.apache.fineract.portfolio.client.domain.ClientStatus;
import org.apache.fineract.portfolio.client.domain.LegalForm; import org.apache.fineract.portfolio.client.domain.LegalForm;
@@ -125,6 +127,8 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final ExternalIdFactory externalIdFactory; private final ExternalIdFactory externalIdFactory;
private final ClientKycWritePlatformService clientKycWritePlatformService; private final ClientKycWritePlatformService clientKycWritePlatformService;
private final ClientKycRepository clientKycRepository;
private final ClientRepository clientJpaRepository;
private final org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLogRepository mobileLookupLogRepository; private final org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLogRepository mobileLookupLogRepository;
private final org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLogRepository sanctionsScreeningLogRepository; private final org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLogRepository sanctionsScreeningLogRepository;
private final CifNumberGenerator cifNumberGenerator; private final CifNumberGenerator cifNumberGenerator;
@@ -189,6 +193,97 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
"Unknown data integrity issue with resource: " + realCause.getMessage()); "Unknown data integrity issue with resource: " + realCause.getMessage());
} }
/**
* Rejects a save when any uniqueness-relevant field (mobile/email on the client row, PAN/Aadhaar/
* GSTIN on the KYC row) already exists on another active client. Runs BEFORE saveAndFlush and
* BEFORE the external KYC API call, so the vendor isn't billed on duplicate-identity records.
*
* @param command the JsonCommand carrying the client payload (PAN/Aadhaar/GSTIN nested
* under the {@code kyc} object)
* @param excludeClientId {@code null} on create; the row id on update so a row doesn't conflict
* with itself
*/
private void validateClientUniqueness(final JsonCommand command, final Long excludeClientId) {
final String mobileNo = command.stringValueOfParameterNamed(ClientApiConstants.mobileNoParamName);
if (StringUtils.isNotBlank(mobileNo)) {
final boolean clash = excludeClientId == null
? clientJpaRepository.existsByMobileNo(mobileNo)
: clientJpaRepository.existsByMobileNoExcluding(mobileNo, excludeClientId);
if (clash) {
throw new PlatformDataIntegrityException("error.msg.client.duplicate.mobileNo",
"Client with mobileNo `" + mobileNo + "` already exists", "mobileNo", mobileNo);
}
}
final String emailAddress = command.stringValueOfParameterNamed(ClientApiConstants.emailAddressParamName);
if (StringUtils.isNotBlank(emailAddress)) {
final boolean clash = excludeClientId == null
? clientJpaRepository.existsByEmailAddress(emailAddress)
: clientJpaRepository.existsByEmailAddressExcluding(emailAddress, excludeClientId);
if (clash) {
throw new PlatformDataIntegrityException("error.msg.client.duplicate.emailAddress",
"Client with emailAddress `" + emailAddress + "` already exists", "emailAddress", emailAddress);
}
}
// KYC fields are nested under "kyc" in the create/update payload — pull them out manually.
final JsonElement root = this.fromApiJsonHelper.parse(command.json());
if (root == null || !root.isJsonObject()) {
return;
}
final JsonElement kyc = root.getAsJsonObject().get(ClientApiConstants.kycParamName);
if (kyc == null || kyc.isJsonNull() || !kyc.isJsonObject()) {
return;
}
final String pan = this.fromApiJsonHelper.extractStringNamed(
org.apache.fineract.portfolio.client.data.ClientKycApiConstants.panNumberParam, kyc);
if (StringUtils.isNotBlank(pan)) {
final boolean clash = excludeClientId == null
? clientKycRepository.existsByPanNumber(pan)
: clientKycRepository.existsByPanNumberExcluding(pan, excludeClientId);
if (clash) {
throw new PlatformDataIntegrityException("error.msg.client.kyc.duplicate.panNumber",
"A client with PAN `" + pan + "` already exists. PAN must be unique across clients.",
"panNumber", pan);
}
}
// Aadhaar uniqueness: the request carries either the masked form (already trimmed to last-4)
// or the full 12-digit number to be sent to the OTP vendor. We compare against the masked
// column either way — for a full number, mask the leading 8 digits first.
String aadhaar = this.fromApiJsonHelper.extractStringNamed(
org.apache.fineract.portfolio.client.data.ClientKycApiConstants.aadhaarNumberMaskedParam, kyc);
if (StringUtils.isBlank(aadhaar)) {
aadhaar = this.fromApiJsonHelper.extractStringNamed(
org.apache.fineract.portfolio.client.data.ClientKycApiConstants.aadhaarNumberParam, kyc);
}
if (StringUtils.isNotBlank(aadhaar)) {
final String masked = aadhaar.length() == 12 ? "XXXXXXXX" + aadhaar.substring(8) : aadhaar;
final boolean clash = excludeClientId == null
? clientKycRepository.existsByAadhaarMasked(masked)
: clientKycRepository.existsByAadhaarMaskedExcluding(masked, excludeClientId);
if (clash) {
throw new PlatformDataIntegrityException("error.msg.client.kyc.duplicate.aadhaar",
"A client with this Aadhaar already exists. Aadhaar must be unique across clients.",
"aadhaarNumber", masked);
}
}
final String gstin = this.fromApiJsonHelper.extractStringNamed(
org.apache.fineract.portfolio.client.data.ClientKycApiConstants.gstinNumberParam, kyc);
if (StringUtils.isNotBlank(gstin)) {
final boolean clash = excludeClientId == null
? clientKycRepository.existsByGstinNumber(gstin)
: clientKycRepository.existsByGstinNumberExcluding(gstin, excludeClientId);
if (clash) {
throw new PlatformDataIntegrityException("error.msg.client.kyc.duplicate.gstin",
"A client with GSTIN `" + gstin + "` already exists. GSTIN must be unique across clients.",
"gstinNumber", gstin);
}
}
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult createClient(final JsonCommand command) { public CommandProcessingResult createClient(final JsonCommand command) {
@@ -198,6 +293,10 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
this.fromApiJsonDeserializer.validateForCreate(command.json()); this.fromApiJsonDeserializer.validateForCreate(command.json());
// Reject duplicates BEFORE save AND before the external KYC API call further down — we
// don't want to burn vendor credits on identity-duplicate records.
validateClientUniqueness(command, null);
final Boolean isAddressEnabled = configurationDomainService.isAddressEnabled(); final Boolean isAddressEnabled = configurationDomainService.isAddressEnabled();
final Long officeId = command.longValueOfParameterNamed(ClientApiConstants.officeIdParamName); final Long officeId = command.longValueOfParameterNamed(ClientApiConstants.officeIdParamName);
@@ -637,6 +736,10 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
this.context.validateAccessRights(clientHierarchy); this.context.validateAccessRights(clientHierarchy);
// Same duplicate guard as createClient — exclude the current row so non-uniqueness
// changes (e.g. address-only update) don't trip the check against itself.
validateClientUniqueness(command, clientId);
final Map<String, Object> changes = new LinkedHashMap<>(9); final Map<String, Object> changes = new LinkedHashMap<>(9);
if (command.isChangeInIntegerParameterNamed(ClientApiConstants.statusParamName, clientForUpdate.getStatus())) { if (command.isChangeInIntegerParameterNamed(ClientApiConstants.statusParamName, clientForUpdate.getStatus())) {
@@ -308,4 +308,5 @@
<include file="parts/0294_add_tfa_token_to_dsc_challenge.xml" relativeToChangelogFile="true"/> <include file="parts/0294_add_tfa_token_to_dsc_challenge.xml" relativeToChangelogFile="true"/>
<include file="parts/0295_unique_email_mobile_on_m_appuser.xml" relativeToChangelogFile="true"/> <include file="parts/0295_unique_email_mobile_on_m_appuser.xml" relativeToChangelogFile="true"/>
<include file="parts/0296_unique_nominee_per_account_type.xml" relativeToChangelogFile="true"/> <include file="parts/0296_unique_nominee_per_account_type.xml" relativeToChangelogFile="true"/>
<include file="parts/0297_seed_indian_states.xml" relativeToChangelogFile="true"/>
</databaseChangeLog> </databaseChangeLog>
@@ -0,0 +1,294 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<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">
<!--
Seed the full set of 28 Indian States + 8 Union Territories under the STATE code so
m_address.state_province_id resolution works for every PIN that the
api.postalpincode.in lookup returns.
Before this migration only MAHARASHTRA / MADHYA_PRADESH / DELHI existed and the
names were UPPER_SNAKE_CASE — postal API returns Title Case With Spaces
("Madhya Pradesh"), so anything except a single-word match silently failed and the
client address ended up with state_province_id = NULL.
Change A renames the 3 existing rows in place (preserves their ids and any FKs
already pointing at them — historical client addresses stay linked).
Change B inserts the missing 33 (25 states + 8 UTs).
Names are the canonical postal forms so the UI's case-insensitive name match works
directly against api.postalpincode.in output.
-->
<!-- =========================== A: rename existing 3 =========================== -->
<changeSet id="0297a_rename_maharashtra" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="1">
SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id
WHERE c.code_name = 'STATE' AND cv.code_value = 'MAHARASHTRA'
</sqlCheck>
</preConditions>
<update tableName="m_code_value">
<column name="code_value" value="Maharashtra"/>
<column name="code_description" value="Indian State"/>
<where>code_value = 'MAHARASHTRA'
AND code_id = (SELECT id FROM m_code WHERE code_name = 'STATE')</where>
</update>
</changeSet>
<changeSet id="0297a_rename_madhya_pradesh" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="1">
SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id
WHERE c.code_name = 'STATE' AND cv.code_value = 'MADHYA_PRADESH'
</sqlCheck>
</preConditions>
<update tableName="m_code_value">
<column name="code_value" value="Madhya Pradesh"/>
<column name="code_description" value="Indian State"/>
<where>code_value = 'MADHYA_PRADESH'
AND code_id = (SELECT id FROM m_code WHERE code_name = 'STATE')</where>
</update>
</changeSet>
<changeSet id="0297a_rename_delhi" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="1">
SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id
WHERE c.code_name = 'STATE' AND cv.code_value = 'DELHI'
</sqlCheck>
</preConditions>
<update tableName="m_code_value">
<column name="code_value" value="Delhi"/>
<column name="code_description" value="Union Territory"/>
<where>code_value = 'DELHI'
AND code_id = (SELECT id FROM m_code WHERE code_name = 'STATE')</where>
</update>
</changeSet>
<!-- =========================== B: insert the missing 33 ====================== -->
<!--
Each row is guarded by a sqlCheck so the migration is idempotent: re-running it
won't create duplicates and won't fail when one is already present.
Names follow api.postalpincode.in's exact spellings.
-->
<!-- ── States (25) ─────────────────────────────────────────────────────────── -->
<changeSet id="0297b_state_andhra_pradesh" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Andhra Pradesh'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Andhra Pradesh', 'Indian State', 1, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_arunachal_pradesh" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Arunachal Pradesh'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Arunachal Pradesh', 'Indian State', 2, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_assam" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Assam'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Assam', 'Indian State', 3, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_bihar" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Bihar'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Bihar', 'Indian State', 4, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_chhattisgarh" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Chhattisgarh'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Chhattisgarh', 'Indian State', 5, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_goa" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Goa'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Goa', 'Indian State', 6, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_gujarat" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Gujarat'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Gujarat', 'Indian State', 7, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_haryana" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Haryana'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Haryana', 'Indian State', 8, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_himachal_pradesh" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Himachal Pradesh'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Himachal Pradesh', 'Indian State', 9, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_jharkhand" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Jharkhand'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Jharkhand', 'Indian State', 10, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_karnataka" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Karnataka'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Karnataka', 'Indian State', 11, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_kerala" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Kerala'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Kerala', 'Indian State', 12, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_manipur" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Manipur'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Manipur', 'Indian State', 15, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_meghalaya" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Meghalaya'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Meghalaya', 'Indian State', 16, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_mizoram" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Mizoram'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Mizoram', 'Indian State', 17, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_nagaland" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Nagaland'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Nagaland', 'Indian State', 18, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_odisha" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Odisha'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Odisha', 'Indian State', 19, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_punjab" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Punjab'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Punjab', 'Indian State', 20, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_rajasthan" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Rajasthan'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Rajasthan', 'Indian State', 21, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_sikkim" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Sikkim'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Sikkim', 'Indian State', 22, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_tamil_nadu" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Tamil Nadu'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Tamil Nadu', 'Indian State', 23, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_telangana" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Telangana'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Telangana', 'Indian State', 24, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_tripura" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Tripura'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Tripura', 'Indian State', 25, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_uttar_pradesh" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Uttar Pradesh'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Uttar Pradesh', 'Indian State', 26, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_uttarakhand" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Uttarakhand'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Uttarakhand', 'Indian State', 27, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_state_west_bengal" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'West Bengal'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'West Bengal', 'Indian State', 28, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<!-- ── Union Territories (7 new — Delhi already renamed above) ─────────────── -->
<changeSet id="0297b_ut_andaman_nicobar" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Andaman and Nicobar Islands'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Andaman and Nicobar Islands', 'Union Territory', 29, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_ut_chandigarh" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Chandigarh'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Chandigarh', 'Union Territory', 30, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_ut_dnh_dd" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Dadra and Nagar Haveli and Daman and Diu'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Dadra and Nagar Haveli and Daman and Diu', 'Union Territory', 31, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_ut_jammu_kashmir" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Jammu and Kashmir'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Jammu and Kashmir', 'Union Territory', 33, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_ut_ladakh" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Ladakh'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Ladakh', 'Union Territory', 34, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_ut_lakshadweep" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Lakshadweep'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Lakshadweep', 'Union Territory', 35, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
<changeSet id="0297b_ut_puducherry" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON cv.code_id = c.id WHERE c.code_name = 'STATE' AND cv.code_value = 'Puducherry'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) SELECT id, 'Puducherry', 'Union Territory', 36, true, false FROM m_code WHERE code_name = 'STATE'</sql>
</changeSet>
</databaseChangeLog>