feat(election): overall shareholder analysis API + analytics enhancements

- ElectionAnalyticsServiceImpl: fix SQL_FAMILY to count candidate's own family members who are active shareholders (candidate-specific, not system-wide)
- ElectionAnalyticsServiceImpl: SQL_SUB_CASTE now groups by m_client.lastname instead of sub_caste_cv_id
- ElectionAnalyticsServiceImpl: SQL_CANDIDATE_INFO fetches candidate client_id, lastname, religion for match highlighting
- ElectionAnalyticsServiceImpl: add getOverallShareholderAnalysis() — returns all demographic breakdowns with no candidate matching
- ElectionAnalyticsService: add getOverallShareholderAnalysis() interface method
- ElectionCandidateApiResource: add GET /{electionId}/shareholders/analysis endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-30 18:36:02 +05:30
co-authored by Claude Sonnet 4.6
parent 388af0a6a3
commit 99a89d5d81
3 changed files with 54 additions and 12 deletions
@@ -178,6 +178,13 @@ public class ElectionCandidateApiResource {
return Response.ok(analyticsService.getCandidateVoterAnalysis(candidateId)).build();
}
@GET
@Path("/{electionId}/shareholders/analysis")
@Operation(summary = "Get overall demographic analysis of all active shareholders")
public Response getOverallShareholderAnalysis(@PathParam("electionId") Long electionId) {
return Response.ok(analyticsService.getOverallShareholderAnalysis()).build();
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
@@ -11,4 +11,10 @@ public interface ElectionAnalyticsService {
* are naturally aligned with the candidate's profile.
*/
CandidateVoterAnalysisData getCandidateVoterAnalysis(Long candidateId);
/**
* Returns overall demographic analysis of all active shareholders
* without any candidate-specific matching.
*/
CandidateVoterAnalysisData getOverallShareholderAnalysis();
}
@@ -27,11 +27,16 @@ public class ElectionAnalyticsServiceImpl implements ElectionAnalyticsService {
private static final String SQL_CANDIDATE_INFO = """
SELECT ec.id, ec.election_id,
n.applicant_name, n.gender, n.caste_category, n.caste AS sub_caste, n.location, n.date_of_birth,
n.member_id AS candidate_client_id,
mc.lastname AS candidate_lastname,
religion_cv.code_value AS candidate_religion,
p.post_name, p.min_age, p.max_age, p.gender_type, p.area, p.min_share_holding,
COALESCE(NULLIF(ca.city,''), NULLIF(ca.town_village,''), NULLIF(ca.county_district,'')) AS candidate_city
FROM m_election_candidate ec
JOIN m_election_nomination n ON ec.nomination_id = n.id
JOIN m_election_post p ON ec.post_id = p.id
JOIN m_client mc ON mc.id = n.member_id
LEFT JOIN m_code_value religion_cv ON religion_cv.id = mc.religion_cv_id
LEFT JOIN LATERAL (
SELECT ma.city, ma.town_village, ma.county_district
FROM m_client_address mca
@@ -175,15 +180,14 @@ public class ElectionAnalyticsServiceImpl implements ElectionAnalyticsService {
AND c.date_of_birth IS NOT NULL
""";
// ── Sub-caste breakdown ───────────────────────────────────────────────────
// ── Sub-caste breakdown — grouped by client last name ─────────────────────
private static final String SQL_SUB_CASTE = """
SELECT COALESCE(cv.code_value, 'Not Specified') AS label,
SELECT COALESCE(NULLIF(c.lastname, ''), 'Not Specified') AS label,
COUNT(DISTINCT sa.client_id) AS cnt
FROM m_share_account sa
JOIN m_client c ON sa.client_id = c.id AND c.status_enum = 300
LEFT JOIN m_code_value cv ON c.sub_caste_cv_id = cv.id
WHERE sa.status_enum = 300
GROUP BY cv.code_value
GROUP BY c.lastname
ORDER BY cnt DESC
""";
@@ -270,14 +274,16 @@ public class ElectionAnalyticsServiceImpl implements ElectionAnalyticsService {
""";
// ── Family members ────────────────────────────────────────────────────────
// Counts candidate's own family members who are also active shareholders
private static final String SQL_FAMILY = """
SELECT
COUNT(DISTINCT CASE WHEN fm.id IS NOT NULL THEN sa.client_id END) AS with_family,
COUNT(DISTINCT CASE WHEN sa.status_enum = 300 THEN fm.linked_client_id END) AS with_family,
COUNT(fm.id) AS total_family
FROM m_share_account sa
JOIN m_client c ON sa.client_id = c.id AND c.status_enum = 300
LEFT JOIN m_family_members fm ON fm.client_id = c.id
WHERE sa.status_enum = 300
FROM m_family_members fm
LEFT JOIN m_share_account sa ON sa.client_id = fm.linked_client_id
WHERE fm.client_id = ?
AND fm.is_deleted = false
AND fm.is_active = true
""";
@Override
@@ -294,8 +300,10 @@ public class ElectionAnalyticsServiceImpl implements ElectionAnalyticsService {
String candidateName = str(info, "applicant_name");
String candidateGender = str(info, "gender");
String candidateCaste = str(info, "caste_category");
String candidateSubCaste = str(info, "sub_caste");
String candidateSubCaste = str(info, "candidate_lastname");
String candidateReligion = str(info, "candidate_religion");
String candidateLocation = str(info, "location");
Long candidateClientId = toLong(info, "candidate_client_id");
Long electionId = toLong(info, "election_id");
String postName = str(info, "post_name");
Integer postMinAge = toInt(info, "min_age");
@@ -361,7 +369,7 @@ public class ElectionAnalyticsServiceImpl implements ElectionAnalyticsService {
.filter(BreakdownItem::isMatch).mapToLong(BreakdownItem::getCount).sum();
// ── Religion ───────────────────────────────────────────────────────────
List<BreakdownItem> religionBreakdown = buildBreakdown(SQL_RELIGION, total, null, false);
List<BreakdownItem> religionBreakdown = buildBreakdown(SQL_RELIGION, total, candidateReligion, true);
// ── Occupation ─────────────────────────────────────────────────────────
List<BreakdownItem> occupationBreakdown = buildBreakdown(SQL_OCCUPATION, total, null, false);
@@ -379,7 +387,7 @@ public class ElectionAnalyticsServiceImpl implements ElectionAnalyticsService {
.mapToLong(BreakdownItem::getCount).sum();
// ── Family members ─────────────────────────────────────────────────────
Map<String, Object> familyRow = jdbcTemplate.queryForMap(SQL_FAMILY);
Map<String, Object> familyRow = jdbcTemplate.queryForMap(SQL_FAMILY, candidateClientId);
long shareholdersWithFamily = toLong(familyRow, "with_family");
long totalFamilyMembers = toLong(familyRow, "total_family");
@@ -593,6 +601,27 @@ public class ElectionAnalyticsServiceImpl implements ElectionAnalyticsService {
return items;
}
@Override
@Transactional(readOnly = true)
public CandidateVoterAnalysisData getOverallShareholderAnalysis() {
long total = jdbcTemplate.queryForObject(SQL_TOTAL, Long.class);
return CandidateVoterAnalysisData.builder()
.totalActiveShareholders(total)
.genderBreakdown(buildBreakdown(SQL_GENDER, total, null, false))
.ageBreakdown(buildAgeBreakdown(total, null))
.religionBreakdown(buildBreakdown(SQL_RELIGION, total, null, false))
.casteBreakdown(buildBreakdown(SQL_CASTE, total, null, false))
.subCasteBreakdown(buildBreakdown(SQL_SUB_CASTE, total, null, false))
.occupationBreakdown(buildBreakdown(SQL_OCCUPATION, total, null, false))
.educationBreakdown(buildBreakdown(SQL_EDUCATION, total, null, false))
.maritalBreakdown(buildBreakdown(SQL_MARITAL, total, null, false))
.shareHoldingBreakdown(buildBreakdown(SQL_SHARE_HOLDING, total, null, false))
.tenureBreakdown(buildBreakdown(SQL_TENURE, total, null, false))
.areaBreakdown(buildBreakdown(SQL_AREA, total, null, false))
.build();
}
private static String str(Map<String, Object> row, String key) {
Object v = row.get(key);
return v == null ? null : v.toString().trim();