feat(election): candidate voter analysis API — multi-dimension breakdown + sub-caste migration

- New ElectionAnalyticsServiceImpl with 14 SQL breakdown queries:
  gender, age, area, social category (caste), sub-caste, religion,
  occupation, share-holding, tenure, loan, education, marital status,
  monthly income, savings account depth
- Cross-dimension match: caste + area simultaneously (core voter base)
- Zero-count row injection for caste/gender/sub-caste when candidate's
  category has no shareholder matches in GROUP BY result
- Liquibase 0343: sub_caste_cv_id column on m_client with 33 pre-seeded
  sub-caste values across General / OBC / SC / ST categories
- Liquibase 0342: election post eligibility columns
- CandidateVoterAnalysisData DTO with education, marital, income,
  savings, activeSavingsHolders, crossMatchCount fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-30 11:54:01 +05:30
co-authored by Claude Sonnet 4.6
parent 34f84e1fc6
commit 388af0a6a3
7 changed files with 941 additions and 0 deletions
@@ -369,4 +369,6 @@
<include file="parts/0339_election_ceo_precheck_family_loan_check.xml" relativeToChangelogFile="true"/>
<include file="parts/0340_election_candidate_symbol_text.xml" relativeToChangelogFile="true"/>
<include file="parts/0341_election_audit_log.xml" relativeToChangelogFile="true"/>
<include file="parts/0342_election_post_add_eligibility_columns.xml" relativeToChangelogFile="true"/>
<include file="parts/0343_client_sub_caste_column.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,59 @@
<?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">
<!-- Add gender_type if not present -->
<changeSet id="0342_election_post_add_gender_type" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_election_post" columnName="gender_type"/></not>
</preConditions>
<addColumn tableName="m_election_post">
<column name="gender_type" type="VARCHAR(20)" defaultValue="ANY">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<!-- Add min_age if not present -->
<changeSet id="0342_election_post_add_min_age" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_election_post" columnName="min_age"/></not>
</preConditions>
<addColumn tableName="m_election_post">
<column name="min_age" type="INT"/>
</addColumn>
</changeSet>
<!-- Add max_age if not present -->
<changeSet id="0342_election_post_add_max_age" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_election_post" columnName="max_age"/></not>
</preConditions>
<addColumn tableName="m_election_post">
<column name="max_age" type="INT"/>
</addColumn>
</changeSet>
<!-- Add min_share_holding if not present -->
<changeSet id="0342_election_post_add_min_share_holding" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_election_post" columnName="min_share_holding"/></not>
</preConditions>
<addColumn tableName="m_election_post">
<column name="min_share_holding" type="INT" defaultValueNumeric="0"/>
</addColumn>
</changeSet>
<!-- Add area if not present -->
<changeSet id="0342_election_post_add_area" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_election_post" columnName="area"/></not>
</preConditions>
<addColumn tableName="m_election_post">
<column name="area" type="VARCHAR(200)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,138 @@
<?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">
<!-- Add SubCaste master code -->
<changeSet id="0343-1" author="election-analytics">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code WHERE code_name = 'SubCaste'</sqlCheck>
</preConditions>
<sql>INSERT INTO m_code (code_name, is_system_defined) VALUES ('SubCaste', false)</sql>
</changeSet>
<!-- Add sub-caste values: General -->
<changeSet id="0343-2" author="election-analytics">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM m_code_value cv
JOIN m_code mc ON cv.code_id = mc.id
WHERE mc.code_name = 'SubCaste' AND cv.code_value = 'Brahmin'
</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT mc.id, vals.code_value, vals.pos, true, false
FROM m_code mc,
(VALUES
('Brahmin', 1),
('Kshatriya / Rajput', 2),
('Vaishya / Baniya', 3),
('Kayastha', 4),
('Jat', 5),
('Sindhi', 6),
('Other General', 7)
) AS vals(code_value, pos)
WHERE mc.code_name = 'SubCaste';
</sql>
</changeSet>
<!-- Add sub-caste values: OBC -->
<changeSet id="0343-3" author="election-analytics">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM m_code_value cv
JOIN m_code mc ON cv.code_id = mc.id
WHERE mc.code_name = 'SubCaste' AND cv.code_value = 'Yadav'
</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT mc.id, vals.code_value, vals.pos, true, false
FROM m_code mc,
(VALUES
('Yadav', 10),
('Kurmi', 11),
('Teli', 12),
('Nai / Napit', 13),
('Kewat / Dhimar', 14),
('Kumhar', 15),
('Kushwaha / Patel',16),
('Mali', 17),
('Lohar', 18),
('Prajapati', 19),
('Banjara', 20),
('Other OBC', 21)
) AS vals(code_value, pos)
WHERE mc.code_name = 'SubCaste';
</sql>
</changeSet>
<!-- Add sub-caste values: SC -->
<changeSet id="0343-4" author="election-analytics">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM m_code_value cv
JOIN m_code mc ON cv.code_id = mc.id
WHERE mc.code_name = 'SubCaste' AND cv.code_value = 'Chamar / Jatav'
</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT mc.id, vals.code_value, vals.pos, true, false
FROM m_code mc,
(VALUES
('Chamar / Jatav', 30),
('Balmiki', 31),
('Dhobi', 32),
('Pasi', 33),
('Meghwal', 34),
('Kori', 35),
('Other SC', 36)
) AS vals(code_value, pos)
WHERE mc.code_name = 'SubCaste';
</sql>
</changeSet>
<!-- Add sub-caste values: ST -->
<changeSet id="0343-5" author="election-analytics">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM m_code_value cv
JOIN m_code mc ON cv.code_id = mc.id
WHERE mc.code_name = 'SubCaste' AND cv.code_value = 'Gond'
</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT mc.id, vals.code_value, vals.pos, true, false
FROM m_code mc,
(VALUES
('Gond', 40),
('Bhil', 41),
('Korku', 42),
('Baiga', 43),
('Bharia', 44),
('Sahariya',45),
('Other ST',46)
) AS vals(code_value, pos)
WHERE mc.code_name = 'SubCaste';
</sql>
</changeSet>
<!-- Add sub_caste_cv_id column to m_client -->
<changeSet id="0343-6" author="election-analytics">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client" columnName="sub_caste_cv_id"/></not>
</preConditions>
<addColumn tableName="m_client">
<column name="sub_caste_cv_id" type="BIGINT">
<constraints nullable="true"
foreignKeyName="fk_client_sub_caste"
references="m_code_value(id)"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>