From 99a89d5d81baf1f4a278d12ee0182f894e0c67b6 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Tue, 30 Jun 2026 18:36:02 +0530 Subject: [PATCH] feat(election): overall shareholder analysis API + analytics enhancements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../api/ElectionCandidateApiResource.java | 7 +++ .../service/ElectionAnalyticsService.java | 6 +++ .../service/ElectionAnalyticsServiceImpl.java | 53 ++++++++++++++----- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionCandidateApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionCandidateApiResource.java index 9f4dfd982..de58cbeb3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionCandidateApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionCandidateApiResource.java @@ -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 // ------------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionAnalyticsService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionAnalyticsService.java index 379fc66b4..35631dde4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionAnalyticsService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionAnalyticsService.java @@ -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(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionAnalyticsServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionAnalyticsServiceImpl.java index 6b635fa4f..9b8486b55 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionAnalyticsServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionAnalyticsServiceImpl.java @@ -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 religionBreakdown = buildBreakdown(SQL_RELIGION, total, null, false); + List religionBreakdown = buildBreakdown(SQL_RELIGION, total, candidateReligion, true); // ── Occupation ───────────────────────────────────────────────────────── List occupationBreakdown = buildBreakdown(SQL_OCCUPATION, total, null, false); @@ -379,7 +387,7 @@ public class ElectionAnalyticsServiceImpl implements ElectionAnalyticsService { .mapToLong(BreakdownItem::getCount).sum(); // ── Family members ───────────────────────────────────────────────────── - Map familyRow = jdbcTemplate.queryForMap(SQL_FAMILY); + Map 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 row, String key) { Object v = row.get(key); return v == null ? null : v.toString().trim();