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:
co-authored by
Claude Sonnet 4.6
parent
388af0a6a3
commit
99a89d5d81
+7
@@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
+6
@@ -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();
|
||||
}
|
||||
|
||||
+41
-12
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user